场景
在写代码的时候时常有个问题困扰着我,在打印某个变量时,经常会忘记之前打印的是哪个变量,在同一段代码里面打印多个变量尤为明显。经常会一脸懵逼,咦,我刚刚要打印的变量是第几个来着?所以一版会在前面加个标识例如 :`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语法树就可以完成想要的效果,我们可以通过https://astexplorer.net/这个网站看到代码的AST语法树,然后安装Babel提供的babel-types工具库操作抽象语法树(AST)的节点。
例如console.log(something)
的AST是这样的:
从抽象语法树上可以看到,console.log()
的type
是CallExpression
,然后callee
的类型是MemberExpression
,它的成员分别是console
和log
,这个方法调用的入参就是arguments
数组,
所以只要在arguments
插入对应的字符串节点就能实现我想要的效果。代码
```javascript // 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`里面配置上这个插件:
```javascript
// 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:https://www.npmjs.com/package/babel-plugin-better-log