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 代码的转换器,欢迎大家查阅。