image.png

场景

  1. 在写代码的时候时常有个问题困扰着我,在打印某个变量时,经常会忘记之前打印的是哪个变量,在同一段代码里面打印多个变量尤为明显。经常会一脸懵逼,咦,我刚刚要打印的变量是第几个来着?所以一版会在前面加个标识例如 :`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+ 的代码,使它在旧的浏览器或者环境中也能够运行,它的工作流程分为三个部分:

  1. Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
  2. Transform(转换) 对抽象语法树进行转换。
  3. Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码。

image.png所以只要操作 抽象语法树 上的节点就可以实现想要的效果。

babel插件机制是通过访问者模式实现的:

  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • Visitor 的对象定义了用于 AST 中获取具体节点的方法
  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

    实现方法

    所以通过查看AST的type类型,就知道对应的方法名,在里面改写AST语法树就可以完成想要的效果,我们可以通过https://astexplorer.net/这个网站看到代码的AST语法树,然后安装Babel提供的babel-types工具库操作抽象语法树(AST)的节点。
    例如console.log(something)的AST是这样的:
    image.png
    从抽象语法树上可以看到,console.log()typeCallExpression,然后callee的类型是MemberExpression,它的成员分别是consolelog,这个方法调用的入参就是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 } }

  1. `babel.config.js`里或者 `webpack``babel-loader`里面配置上这个插件:
  2. ```javascript
  3. // babel.config.js
  4. const path = require('path');
  5. module.exports = {
  6. // ...
  7. plugins:[
  8. [path.resolve(__dirname, 'babel-plugin-log.js')]
  9. ]
  10. // ...
  11. }

这是原来的源代码:
image.png
经过插件后打包编译后就是这样的效果:
image.png

后记

  1. 可以用process.env.xxx === 'development' ? ['babel-plugin-log'] : []来规避生产环境打包的话使用到了这个插件:
  2. 还可以用 log(...)代替console.log(...),然后在这个插件里面补全前面console.的抽象语法树,这样能少写几个字母…
  3. logRed(...)这样的语法,在插件里面补全对应的语法树信息,让你的log在控制台显示出红色等等…

本文代码已发布到npm和github:https://www.npmjs.com/package/babel-plugin-better-log