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

lxf2023-03-13 18:33:01

本文正在参加「 . 」

前言

相信大家项目开发中经常会使用 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,漏怕笨。

结束语

如果小伙伴们有别的想法,欢迎留言,让我们共同学习进步