一个插件解决console.log()使用体验~

735 阅读4分钟

image.png

场景

在写代码的时候时常有个问题困扰着我,在打印某个变量时,经常会忘记之前打印的是哪个变量,在同一段代码里面打印多个变量尤为明显。经常会一脸懵逼,咦,我刚刚要打印的变量是第几个来着?所以一版会在前面加个标识例如 :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语法树就可以完成想要的效果,我们可以通过astexplorer.net/这个网站看到代码的AST语法树,然后安装Babel提供的babel-types工具库操作抽象语法树(AST)的节点。
例如console.log(something)的AST是这样的:
image.png
从抽象语法树上可以看到,console.log()typeCallExpression,然后callee的类型是MemberExpression,它的成员分别是consolelog,这个方法调用的入参就是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里或者 webpackbabel-loader里面配置上这个插件:

// babel.config.js
const path = require('path');

module.exports = {
  // ...
  plugins:[
    [path.resolve(__dirname, 'babel-plugin-log.js')]
  ]
  // ...
}

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

后记

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

如果觉得对你有用麻烦点个赞吧~

本文代码已发布到npm和github:www.npmjs.com/package/bab…