阅读 203

写个 Babel 插件丰富你的 console 内容

console.log 相信很多人都用过,作为平时工作中主力调试工具,我常常有些困惑,就是如何找到控制台中打印的信息对应的源码。通常情况会在打印的信息之前加入一些字符串,如下所示:

console.log('from handleFileUpload---->', data);
复制代码

那么有没有更好的方式来满足这个需求呢?最好是自动添加信息。因为最近在研究ast,所以就想为什么不能通过 Babel 在编译源码的过程中,向 console 的参数自动添加我想要的信息呢,于是乎就有了这个插件。

Babel 的基本信息我就不在这里叙述了,关于Babel有很多非常棒的资料,而本文重点主要阐述这个插件的实现原理。

注:babel-handbook 强烈推荐,Babel作者维护的。

首先需要加入的信息:console 所在的当前文件名、执行上下文/调用栈、行数、列数、用户自定义的信息。

其次还需要明确具体对哪些 console 的方法生效,比如仅仅对 console.log 生效,而 console.warn 不生效。插件中默认生效 console 的方法包括 'debug', 'error', 'exception', 'info', 'log', 'warn'。

最后是需要排除一些文件,比如依赖 node_modules。

既然需求已经明确,那么剩下的就是对应到代码如何实现的问题了。

其中需要添加的信息中,所在文件名可以从 Babel 插件的执行上下文中的 filename 属性获得,行数列数可以在 Babel 转换的 AST 中的 node 中获得,用户自定义内容可以通过插件提供出去的配置接口获得。

获取所在的调用栈相对比较麻烦,一般调用栈中的 AST 节点会包括函数声明、变量声明、对象属性、对象方法、类方法等等,例如下面的代码:

class Foo {
  bar() {
    const help = () => {
      console.info('banana');
    }
  }
}
复制代码

console.info('banana') 向上计算调用栈,分别是函数声明节点 help、类方法节点 bar、类 Foo。我们可以通过 path 的 findParent 向上查找满足类型条件的父节点,并获取其中的 name。

注:Babel 的每个节点 node 对应都有一个 path,链表结构,可以通过链表串联起 AST 树中的所有节点,和类似 React 中的 ReactElement 和 Fiber 的关系 。

相关代码如下,利用递归将向上满足条件的节点 name 放在一个数组中:

const scopeHandlers = {
  FunctionDeclaration: path => `${path.node.id.name}()`,
  VariableDeclarator: path => path.node.id.name,
  ObjectProperty: path => path.node.key.name,
  ObjectMethod: path => `${path.node.key.name}()`,
  ClassMethod: path => `${path.node.key.name}()`,
  ClassExpression: path => path.node.id.name,
  ClassDeclaration: path => path.node.id.name,
  AssignmentExpression: path => path.node.left.name
};

export function computeContext(path, scope = []) {
  const parentPath = path.findParent(path =>
    Object.keys(scopeHandlers).includes(path.type)
  );
  if (parentPath) {
    return computeContext(parentPath, [
      scopeHandlers[parentPath.type](parentPath),
      ...scope
    ]);
  }
  return scope.length ? `${scope.join(' -> ')}` : '';
}
复制代码

至于限制只对部分文件中的 console 方法生效,或只对 console 中的某个方法生效就很简单了,只需要在插件方法前面做参数校验即可。

这样一个简单的 Babel 插件就实现了,主体代码如下:

import computeOptions from './utils/pluginOption';
import { isObject, matchesExclude, computeContext } from './utils/tools';

export default function({ types: t }) {
  const visitor = {
    CallExpression(path) {
      if (
        t.isMemberExpression(path.node.callee) &&
        path.node.callee.object.name === 'console'
      ) {
        // options need to be an object
        if (this.opts && !isObject(this.opts)) {
          return console.error(
            '[babel-plugin-console-enhanced]: options need to be an object.'
          );
        }

        const options = computeOptions(this.opts);

        const filename = this.filename || this.file.opts.filename || 'unknown';

        // not work on an excluded file
        if (
          Array.isArray(options.exclude) &&
          options.exclude.length &&
          matchesExclude(options.exclude, filename)
        ) {
          return;
        }

        // not work on a non-inlcuded method
        if (!options.methods.includes(path.node.callee.property.name)) {
          return;
        }

        let description = '';

        if (options.addFilename) {
          description = `${description}filename: ${filename}, `;
        }

        if (options.addCodeLine) {
          const line = path.node.loc.start.line;
          description = `${description}line: ${line}, `;
        }

        if (options.addCodeColumn) {
          const column = path.node.loc.start.column;
          description = `${description}column: ${column}, `;
        }

        if (options.addContext) {
          const scope = computeContext(path);
          description = scope
            ? `${description}context: ${scope}, `
            : description;
        }

        if (options.customContent) {
          description = `${description}${options.customContent}, `;
        }

        if (description) {
          path.node.arguments.unshift(t.stringLiteral(description));
        }
      }
    }
  };

  return {
    name: 'babel-plugin-console-enhanced',
    visitor
  };
}
复制代码

相关代码库: github.com/mcuking/bab…

另外最近正在写一个编译 Vue 代码到 React 代码的转换器,欢迎大家查阅。

github.com/mcuking/vue…

关注下面的标签,发现更多相似文章
评论