前言
前端开发调试代码时,经常会用到console
来打log,生产环境中也经常用console
来加诊断log或输出error log。
但是使用时经常发现,如果log比较多,不知道每个log啥时候打的,也不知道是哪个文件打出来的,于是就想到写一个自动编译功能,在不改动项目代码的情况下(少部分改动),增强console输出的信息。
需求
给每个console输出前面加时间和代码位置信息前缀。如下图。
console.log(123);
console.warn('abc');
console.log(123, 'abc', true, [1, 2, 3], { key: 'value' });
Web
先搭建一个简易Webpack
构建工程。(其实也可以用babel命令行直接编译)
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
},
...
module: {
rules: [{
test: /\.(jsx|js)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"],
}
}
}],
},
...
plugins: [
new HtmlWebpackPlugin(),
],
};
加一个index.js
为了编译用,加个button点击事件,输出三个console.log
。
// src/index.js
(function () {
const btn1 = document.createElement('button');
btn1.textContent = 'button1';
document.body.appendChild(btn1);
btn1.onclick = function () {
console.log(123);
console.warn('abc');
console.log(123, 'abc', true, [1, 2, 3], { key: 'value' });
}
})();
Babel plugin
参考文档:
- AST explorer AST语法转换器
- Babel AST Explorer 基于Babel的AST语法转换器
- Babel · Babel Babel官方文档
根路径下创建一个my-babel-pluin.js
,然后引用到Webpack
babel-loader
里的plugins
中。
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"],
plugins: ["./my-babel-pluin.js"],
}
然后执行webpack build时,就可以使用这个自定义的babel plugin了。下面来写plugin,有两个方案。
方案1
用到了Babel
的@babel/types
:babeljs.io/docs/babel-…
const basename = require("path").basename;
module.exports = (babel) => {
const { types: t, template } = babel;
return {
name: 'test-log',
visitor: {
CallExpression(path, state) {
if (t.isMemberExpression(path.node.callee) // 判断是不是对象成员,比如console.log
&& path.node.callee.object.name === 'console'
&& ['log', 'info', 'warn', 'error'].includes(path.node.callee.property.name)
) {
const filename = basename(state.file.opts.filename); // index.js
const location = `${path.node.loc.start.line}:${path.node.loc.start.column}`; // 7:8
const date = t.identifier('Date'); // Date
const newDate = t.newExpression(date, []); // new Date()
const toString = t.memberExpression(newDate, t.identifier('toLocaleString')); // new Date().toLocaleString
const localDate = t.callExpression(toString, []); // new Date().toLocaleString()
const left = t.binaryExpression("+", t.stringLiteral("["), localDate); // "[" + new Date().toLocaleString()
const right = t.stringLiteral(`] [${location}]`); // "] [index.js 7:8]"
const expression = t.binaryExpression("+", left, right); // "[" + new Date().toLocaleString() + "] [index.js 7:8]"
path.node.arguments.unshift(expression); // console.log(expression, ...)
}
}
}
};
}
这里简单介绍下几个知识点,转换步骤可以参考代码里的注释,参考@babel/types 官方文档。
t.stringLiteral
:生成字符串字面量;t.identifier
:生成标识符,比如Date
、console
、变量属性名等等;t.newExpression
:生成创建新对象表达式,也就是用new关键字;t.memberExpression
:生成对象成员,比如属性、方法等,也就是.
;t.callExpression
:生成调用表达式,也就是括号传参;t.binaryExpression
:生成二元表达式,比如上例是把左右两个表达式加起来;path.node.arguments
:当前ast节点表达式的参数数组,比如console.log
这个语法的参数数组,.unshift
意思是在数组第一位插入一个参数。
先npm run build
下看下编译后的代码:
再npm run dev
访问看下效果:
达到了预期。
方案2
方案1使用了各种@babel/types
的api,一步步创建各种语法然后合成到一起,一句很简单的语法就需要很多步骤来生成,特别麻烦,下面介绍一种更简单的生成方式。
@babel/template
:babeljs.io/docs/babel-…
直接上代码:
...
const code = `"[" + new Date().toLocaleString() + "] [${location}]"`;
const expression = template.expression(code)();
path.node.arguments.unshift(expression);
是不是很简单,用一个expression就能生成出来,就相当于直接把代码以字符串形式定义了。编译后的效果也跟方案1一样的。
优化
现在还有个问题,如果代码里的console使用的非常多,会导致编译后的js里有大量的前缀的代码,会导致打包文件体积很大。这时候就会想,能不能把生成前缀的代码单独封装个方法,然后再通过babel plugin转换生成这个方法调用。这样打包后就是一个个方法名了,会有效减少体积。
这里为了方便,加一个window
全局方法,因为location
信息需要编译生成,只有date可以动态生成,所以把new Date().toLocaleString()
直接定义在方法里。
window.my_console = function (method, location, ...args) {
const prefix = `[${new Date().toLocaleString()}] [${location}]`;
console[method](prefix, ...args);
}
然后重写plugin
const window = t.identifier('window'); // window
const my_console = t.memberExpression(window, t.identifier('my_console')); // window.my_console
const args = [t.stringLiteral(path.node.callee.property.name), t.stringLiteral(location), ...path.node.arguments]; // "log", "index.js 7:8", ...args
const expression = t.callExpression(my_console, args); // window.my_console(method, location, ...args)
path.replaceWith(expression); // replace expression
build看下效果:
可以看到console
都替换为window.my_console
,编译后的代码里不会有大量的date字符串了。
其它方案
有的大佬可能会说,“用那么麻烦吗?直接劫持console重写不就行了吗?”,重写console原型方法当然是最简单了,但是相比babel plugin有个问题是,console方法是在window上的,是全局的,会作用于所有执行时的代码,所以也会影响node_modules
下的npm包代码,也会影响通过script引用的cdn代码。
有些需求是只想把自己工程里的代码的console规范输出,不想改变第三方文件里的console,然后再看下上面例子,Webpack里的babel-loader
rule里已经过滤了node_modules
:exclude: /(node_modules|bower_components)/
,所以不会转换node_modules
下的代码,而且只编译Webpack入口文件里的代码,所以也不会影响通过script引用的cdn代码。
总结
本文通过自定义Babel plugin,在编译时转换代码里的console语法,添加时间和代码位置信息,增强console输出格式。
本文知识点:
- AST explorer AST语法转换器
- Babel AST Explorer 基于Babel的AST语法转换器
- Babel · Babel Babel官方文档
- @babel/types · Babel @babel/types
- @babel/template · Babel @babel/template