本文正在参加「金石方案 . 瓜分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,让输出更明晰!

是不是脑瓜疼?

手写一个 Loader

引荐咱们一个在线检查代码 AST 结构的网站:

AST explorer

咱们能够将代码粘贴到这个网站看看生成的结构,因为 console.log函数调用,因此咱们重点看 CallExpression 这个点:

项目开发中输出太紊乱?教你手写 Loader,让输出更明晰!

CallExpression 是函数调用,那 MemberExpression 是什么呢?这儿是 成员表达式 的意思,咱们都知道 Javascript 中,一个目标的成员能够经过 obj.xxx 或者 obj['xxx'] 这两种方式进行访问,这儿的 MemberExpression 就是指代这两种状况,其间,computedtrue 表明是经过 括号 的方式访问的,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 结构:

项目开发中输出太紊乱?教你手写 Loader,让输出更明晰!

能够看到函数的节点类型是 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 节点:

项目开发中输出太紊乱?教你手写 Loader,让输出更明晰!

咱们能够看到 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;
}

最后咱们打包看看效果:

项目开发中输出太紊乱?教你手写 Loader,让输出更明晰!

ok,漏怕笨。

结束语

假如小伙伴们有别的主意,欢迎留言,让咱们一起学习前进。

假如文中有不对的当地,或是咱们有不同的见地,欢迎指出。

假如咱们觉得一切收成,欢迎一键三连。