本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
相信大家项目开发中经常会使用 console
输出,页面中一个两个还好,如果有好多个输出口,就根本不知道谁是谁输出的了,为此,今天我来教大家手写一个 loader,让我们在开发中能够更效率的 debug。
改造前
假设这是我们的代码:
const data = [{
name: 'c1',
age: 12
}, {
name: 'c3',
age: 14
}]
function show(data) {
console.log(data);
}
show(data);
console.log(data);
这是控制台的输出:
是不是脑瓜疼?
手写一个 Loader
推荐大家一个在线查看代码 AST 结构的网站:
我们可以将代码粘贴到这个网站看看生成的结构,由于 console.log
是函数调用,因此我们重点看 CallExpression
这个点:
CallExpression 是函数调用,那 MemberExpression
是什么呢?这里是 成员表达式 的意思,我们都知道 Javascript 中,一个对象的成员可以通过 obj.xxx
或者 obj['xxx']
这两种形式进行访问,这里的 MemberExpression 就是指代这两种情况,其中,computed
为 true
表示是通过 中括号 的形式访问的,false
表示是通过 .
访问的。
为了找到目标节点,我们需要查看节点的 callee.object.name
是否为 console
,如果需要指定是 log
函数,还需要判断 property.name
是否为 log
。
那么通过代码我们怎么知道当前的节点是什么类型呢?
可以通过 @babel/types
这个库。
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports = function (source) {
const ast = parser.parse(source, { sourceType: 'module' });
traverse(ast, {
CallExpression(path) {
const { callee, arguments } = path.node;
if (t.isMemberExpression(callee) && callee.object.name === 'console') {
...
}
}
})
const output = generator(ast, {}, source);
return output.code;
}
traverse
是通过递归深度遍历的,我们先通过 CallExpression 函数命中函数调用的节点,然后通过 isMemberExpression
命中 callee: MemberExpression
的节点。
至此,我们就找到了 console
代码所在的节点,为了让它加上外层函数名,我们需要从这个节点开始,不断向外层寻找最近的父级函数名。
这里我们要通过另一个函数 findParent
来寻找满足条件的父级节点。
我们继续看看生成的 AST 结构:
可以看到函数的节点类型是 FunctionDeclaration 。我们可以通过 path.isFunctionDeclaration
来进行判断。同时,获取到节点后,通过 node.id.name
就可以得到函数名。
if (t.isMemberExpression(callee) && callee.object.name === 'console') {
const parent = path.findParent(p => p.isFunctionDeclaration());
if (parent) {
const fnName = parent.node.id.name
}
}
需要注意的是,这里我们的 console
可能在顶层、匿名函数或是箭头函数进行调用的,这种时候就不会进入内层 if
语句,也不会追加函数名。
万事俱备,只差将函数名插入 console
语句中了,我们回到 CallExpression 节点:
我们可以看到 console.log
函数中已经有一个参数了,也就是我们输出的 data
。为了在 data
前添加函数名,我们要创建一个 字面量节点 ,然后向 arguments
数组头部进行插入。
if (parent) {
const fnName = parent.node.id.name
arguments.unshift(t.stringLiteral(`${fnName}:`));
}
装备 Loader
这里我们使用 webpack 来试试:
...
module.exports = {
resolveLoader: {
modules: [path.resolve(__dirname, '../loaders'), 'node_modules']
},
...
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}, {
loader: 'method-name-loader'
}]
}
},
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin()
]
}
首先通过 resolveLoader
属性告诉 webpack 如果遇到 loader
,先从我们自定义的 loaders 目录开始解析,如果找不到,再去 node_modules
下找;然后针对 js 扩展名的文件使用我们的 loader
。
// method-name-loader.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports = function (source) {
const ast = parser.parse(source, { sourceType: 'module' });
traverse(ast, {
CallExpression(path) {
const { callee, arguments } = path.node;
if (t.isMemberExpression(callee) && callee.object.name === 'console' && callee.property.name === "log") {
const parent = path.findParent(p => p.isFunctionDeclaration());
if (parent) {
const fnName = parent.node.id.name
arguments.unshift(t.stringLiteral(`${fnName}:`));
}
}
}
})
const output = generator(ast, {}, source);
return output.code;
}
最后我们打包看看效果:
ok,漏怕笨。
结束语
如果小伙伴们有别的想法,欢迎留言,让我们共同学习进步💪💪。
如果文中有不对的地方,或是大家有不同的见解,欢迎指出🙏🙏。
如果大家觉得所有收获,欢迎一键三连💕💕。