场景
在写代码的时候时常有个问题困扰着我,在打印某个变量时,经常会忘记之前打印的是哪个变量,在同一段代码里面打印多个变量尤为明显。经常会一脸懵逼,咦,我刚刚要打印的变量是第几个来着?所以一版会在前面加个标识例如 :console.log('someObject ==>', someObject),这样在控制台就会直观一点哪些值是哪些变量的。
思考
每次都要在变量前面加一个标识感觉好麻烦啊,本着能偷懒就偷懒的原则,有没有什么方法能够自动帮我加上某个变量的标志,这样每次就不用自己手写了。这个应该不是很难,只需在编译的的时候做点“手脚”应该就行,接下来就实现的过程。
实现
我们的项目是webpack打包的所以编写个loader,在loader里面做正则字符串替换就行,例如吧字符串的console.log(this.someObject)替换成 console.log('this.someObject ==>', this.someObject),就可以实现类似的效果。但是面对复杂的情况,loader处理起复杂的情况好像有点不称手,例如变量keythis.object[key],多个变量连续打印,做起来不太好用。所以写一个babel-plugin在抽象语法树上做文章或许是更好的选择,接下来就是实现过程。
babel
我们都知 Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行,它的工作流程分为三个部分:
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点。
- Transform(转换) 对抽象语法树进行转换。
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码。
所以只要操作 抽象语法树 上的节点就可以实现想要的效果。
babel插件机制是通过访问者模式实现的:
- 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
- Visitor 的对象定义了用于 AST 中获取具体节点的方法
- Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法
实现方法
所以通过查看AST的type类型,就知道对应的方法名,在里面改写AST语法树就可以完成想要的效果,我们可以通过astexplorer.net/这个网站看到代码的AST语法树,然后安装Babel提供的babel-types工具库操作抽象语法树(AST)的节点。
例如console.log(something)的AST是这样的:
从抽象语法树上可以看到,console.log()的type是CallExpression,然后callee的类型是MemberExpression,它的成员分别是console和log,这个方法调用的入参就是arguments数组,
所以只要在arguments插入对应的字符串节点就能实现我想要的效果。
代码
// babel-plugin-log
// 用来生成或者判断节点的AST语法树的节点
const types = require('@babel/types');
// 递归解析表达式获取变量字符串
function getCallString(obj, name = []) {
if (obj.object) {
name = getCallString(obj.object, name)
}
let realName = ''
if (obj.type === 'ThisExpression') {
realName = 'this'
}
if (obj.type === 'Identifier') {
realName = obj.name
}
if (obj.type === 'MemberExpression') {
realName = obj.property.name || obj.property.value
}
if (realName.indexOf('this') > -1) {
realName = 'this'
}
name = name.concat(realName)
return name
}
// 将字符串插入到表达式前面
function disposeArguments(args = []) {
const newArgs = [] ;
for (let i = 0; i < args.length; i++) {
const item = args[i];
if (item.type === 'StringLiteral') {
newArgs.push(item)
} else {
const callString = getCallString(item, []).join('.')
const notExit = newArgs.some(item => item.type === 'StringLiteral' && item.value.indexOf(callString) > -1)
if (callString && !notExit) {
// 插入遍历字符串节点
const stringLiteral = types.stringLiteral(callString + ' ===>')
newArgs.push(stringLiteral)
}
newArgs.push(item)
if (callString && !notExit) {
// 多个节点增加换行符
newArgs.push(types.stringLiteral('\n'))
}
}
}
return newArgs
}
// babel plugin 访问器对象
const visitor = {
CallExpression(nodePath, state) {
// 当前节点
const { node } = nodePath;
// 判断方法调用节点
if (types.isMemberExpression(node.callee)) {
if (node.callee.object.name === 'console') {
// 判断console.xxx()方法
if (['log', 'info', 'warn', 'error', 'debug'].includes(node.callee.property.name)) {
// 处理抽象语法树
node.arguments = disposeArguments(node.arguments);
}
}
}
}
}
module.exports = function () {
return {
visitor
}
}
在babel.config.js里或者 webpack的babel-loader里面配置上这个插件:
// babel.config.js
const path = require('path');
module.exports = {
// ...
plugins:[
[path.resolve(__dirname, 'babel-plugin-log.js')]
]
// ...
}
这是原来的源代码:
经过插件后打包编译后就是这样的效果:
后记
- 可以用
process.env.xxx === 'development' ? ['babel-plugin-log'] : []来规避生产环境打包的话使用到了这个插件。 - 还可以用
log(...)代替console.log(...),然后在这个插件里面补全前面console.的抽象语法树,这样能少写几个字母... - 用
logRed(...)这样的语法,在插件里面补全对应的语法树信息,让你的log在控制台显示出红色等等...
如果觉得对你有用麻烦点个赞吧~
本文代码已发布到npm和github:www.npmjs.com/package/bab…