如何在不修改Node.js代码库原有函数的情况下为自定义函数添加执行前后日志?
如何在不修改Node.js代码库原有函数的情况下为自定义函数添加执行前后日志?
这需求太接地气了——不想动原有代码,也不想挨个给函数加包装,还得只给自己的代码加日志,用Babel插件绝对是最优解!我来给你一步步拆解实现方法:
核心思路
Babel可以在代码编译阶段修改抽象语法树(AST),自动给我们的函数注入日志代码,全程不用手动修改业务函数,还能通过配置轻松排除node_modules里的第三方代码。
第一步:编写自定义Babel插件
这个插件的作用就是遍历所有函数节点,在函数执行前插入「打印函数名+参数」的代码,在函数返回前插入「打印函数名+返回值」的代码。直接上代码:
module.exports = function({ types: t }) { return { visitor: { // 处理常规函数声明(比如function foo() {}) FunctionDeclaration(path) { injectLogging(path, t); }, // 处理函数表达式(比如const foo = function() {}) FunctionExpression(path) { injectLogging(path, t); }, // 处理箭头函数(比如const foo = () => {}) ArrowFunctionExpression(path) { injectLogging(path, t); } } }; }; function injectLogging(path, t) { // 智能获取函数名,适配匿名函数、变量赋值、对象属性等场景 let functionName = '匿名函数'; if (path.node.id) { functionName = path.node.id.name; } else if (path.parentPath.isVariableDeclarator()) { functionName = path.parentPath.node.id.name; } else if (path.parentPath.isAssignmentExpression()) { if (t.isIdentifier(path.parentPath.node.left.property)) { functionName = path.parentPath.node.left.property.name; } } // 1. 插入执行前的日志:打印函数名和传入的所有参数 const beforeLog = t.expressionStatement( t.callExpression( t.memberExpression(t.identifier('console'), t.identifier('log')), [ t.stringLiteral(`[执行前] 函数: ${functionName}`), t.stringLiteral('参数:'), t.spreadElement(t.identifier('arguments')) ] ) ); // 2. 处理函数体,插入执行后的日志 if (t.isBlockStatement(path.node.body)) { // 把前置日志插到函数体最开头 path.node.body.body.unshift(beforeLog); // 遍历所有return语句,在return前打返回值日志 path.traverse({ ReturnStatement(returnPath) { const returnValue = returnPath.node.argument; const afterLog = t.expressionStatement( t.callExpression( t.memberExpression(t.identifier('console'), t.identifier('log')), [ t.stringLiteral(`[执行后] 函数: ${functionName}`), t.stringLiteral('返回值:'), returnValue || t.identifier('undefined') ] ) ); returnPath.insertBefore(afterLog); } }); // 如果函数没有return语句,在函数末尾打返回undefined的日志 const hasReturn = path.node.body.body.some(node => t.isReturnStatement(node)); if (!hasReturn) { const afterLog = t.expressionStatement( t.callExpression( t.memberExpression(t.identifier('console'), t.identifier('log')), [ t.stringLiteral(`[执行后] 函数: ${functionName}`), t.stringLiteral('返回值:'), t.identifier('undefined') ] ) ); path.node.body.body.push(afterLog); } } else { // 处理箭头函数的表达式体(比如const foo = () => 123),先转成带{}的块级语句 const originalBody = path.node.body; path.node.body = t.blockStatement([ beforeLog, t.returnStatement(originalBody), t.expressionStatement( t.callExpression( t.memberExpression(t.identifier('console'), t.identifier('log')), [ t.stringLiteral(`[执行后] 函数: ${functionName}`), t.stringLiteral('返回值:'), originalBody ] ) ) ]); } }
第二步:配置Babel
在项目根目录创建babel.config.js,把刚才的插件加进去,同时关键要排除node_modules:
module.exports = { presets: ['@babel/preset-env'], // 按需添加你的preset,比如React项目加@babel/preset-react plugins: [require('./path/to/your/log-plugin.js')], // 替换成你的插件路径 exclude: /node_modules/ // 重中之重!不处理第三方模块的函数 };
第三步:运行代码
可以直接用babel-node运行你的代码,或者编译后运行:
- 直接运行:
npx babel-node src/your-entry-file.js
- 或者在package.json里配置scripts:
"scripts": { "dev": "babel-node src/index.js", "build": "babel src --out-dir dist" }
然后执行npm run dev就能看到日志了!
额外说明
- 这个插件适配常规函数、箭头函数、匿名函数、对象方法等绝大多数场景,函数名识别也比较精准;
- 异步函数(async/await)也能完美处理,因为日志是注入到函数体内部的;
- 如果不想给某些函数加日志,可以在插件里加判断逻辑(比如给函数加特定注释,插件识别后跳过)。
备注:内容来源于stack exchange,提问作者Meir
相关产品推荐
相关产品推荐

