本文正在参加「金石方案 . 瓜分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 结构的网站:
AST explorer
咱们能够将代码粘贴到这个网站看看生成的结构,因为 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,漏怕笨。
结束语
假如小伙伴们有别的主意,欢迎留言,让咱们一起学习前进。
假如文中有不对的当地,或是咱们有不同的见地,欢迎指出。
假如咱们觉得一切收成,欢迎一键三连。