实现一个移除 console.log 的 webpack 插件

5,599

本文目的,实现一个可以移除项目中 console.log 的 webpack 插件。借此,我们可以学习一下webpack 插件怎么写。

首先,我们需要初始化环境

yarn init -y
yarn add webpack webpack-cli -D

webpack.config.js 配置如下:

module.exports = {
    entry: './index.js',
    plugins: []
}

根目录下新建一个 index.js,内容如下:

console.log('hello world');

在 package.json 添加 build 命令

"scripts": {
    "build": "yarn webpack"
}

此时我们运行 yarn build 就会在 dist 目录打包出一个 main.js。我们使用 node 执行这个文件就会输出 hello world

node dist/main.js

如果成功输出,就说明环境已经配置好了。

目录结构如下(你可能没有 README.md,不过没有关系):

为了方便期间,我们的自定义 Plugin 就放到 webpack.config.js 这个文件里。

切换到 webpack.config.js 我们新建一个类,作为我们的自定义 Plugin。

class RemoveLogs {
    constructor(options) {
        this.options = options
    }

    apply(compiler) {
        console.log("Hello from the custom plugin")
    };
}

module.exports = {
    entry: './index.js',
    plugins: [ new RemoveLogs()]
}

此时再运行 yarn build,你会在控制台看到 Hello from the custom plugin 这句话。这说明我们的自定义 Plugin 已经添加成功了。

我们接下来要做的,就是把我们代码里的 console.log 语句去掉,我们可以选择很多时机来做这件事。比如在即将开始编译的时候,而我们这里选择放在编译结束之后。

class RemoveLogs {
    constructor(options) {
        this.options = options
    }

    apply(compiler) {
        compiler.hooks.done.tap("RemoveLogs", stats => {
            console.log('我将要移除所有的 console')
            removeAllLogs(stats)
        });
    };
}

compiler.hooks.done 类似于我们 DOM 操作中的 document.addEventListener('click', ()=>{}) 。也就是说我们想在编译结束后要做什么事。其实还有非常多的生命周期,具体可以点击此链接

compiler.hooks.done.tap 这个函数的第一个参数是字符串,这里是 RemoveLogs。它在调试的时候比较有用,你可以随便起名,并不需要一定要对应 class 名;第二个参数是到了这个生命周期会执行的函数。

对于按钮我们可以使用 addEventListener添加多个 click 事件,在这里你也可以添加任意多的事件。不过,这里我们就只加一个就好了。

接下来我们完善 removeAllLogs 这个函数:

removeAllLogs(stats) {
    const { path, filename } = stats.compilation.options.output;
    let filePath = path + "/" + filename;
    
    fs.readFile(filePath, "utf8", (err, data) => {
        const rgx = /console.log\(['|"](.*?)['|"]\)/;
        const newData = data.replace(rgx, "");
        if (err) console.log(err);
        fs.writeFile(filePath, newData, function (err) {
            if (err) {
                return console.log(err)
            }
            console.log("Logs Removed");
        });
    });
}

但是此时运行会报错,因为这个时候的 filename 获取到的是一个替代值,具体来说,是字符串 '[name]',你可以自己运行试试看。我们还需要加一个钩子函数来获取此时真实的 filename。

compiler.hooks.compilation.tap('GetFileNamePlugin', compilation => {
    compilation.hooks.chunkIds.tap('GetFileNamePlugin', (c) => {
        this.filename = Array.from(c)[0].name
    });
});

此时我们的 removeAllLogs 也相应改变获取 filePath 的方式:

let filePath = (path + "/" + filename).replace(/\[name\]/g, this.filename); 

到这里,我们就实现完毕了。试着运行 node dist/main.js,会发现我们已经没有任何输出了。

就这样,我们就实现完了一个最简单的 webpack 插件,有没有感觉并不是很难呢:)

评论里有同学说使用正则匹配来去除 console 的这种方法并不好,并且推荐在 babel 里做这件事,我在他的建议下找到了一个做这件事的 babel 插件: babel-plugin-transform-remove-console

同时它的源码在这:源码

因为这已经不是本文的主题了,就不在这里展开了,有兴趣的同学可以自己去研究~

本篇文章的全部代码如下:

const fs = require('fs')

class RemoveLogs {
    constructor(options) {
        this.options = options
    }

    apply(compiler) {
        console.log(compiler.options.output);
        compiler.hooks.done.tap("RemoveLogs", stats => {
            try {
                this.removeAllLogs(stats);
            } catch (e) {
                console.log(e);
            }
        });

        compiler.hooks.compilation.tap('HelloCompilationPlugin', compilation => {
            compilation.hooks.chunkIds.tap('HelloCompilationPlugin', (c) => {
                this.filename = Array.from(c)[0].name
            });
        });
    };
    removeAllLogs(stats) {
        const { path, filename } = stats.compilation.options.output;
        let filePath = (path + "/" + filename).replace(/\[name\]/g, this.filename);

        try {
            fs.readFile(filePath, "utf8", (err, data) => {
                const rgx = /console.log\(['|"](.*?)['|"]\)/;
                const newData = data.replace(rgx, "");
                if (err) console.log(err);
                fs.writeFile(filePath, newData, function (err) {
                    if (err) {
                        console.log(err)
                    }
                    console.log("all logs Removed");
                });
            });
        } catch (e) {
            console.error(e)
        }

    }
}

module.exports = {
    entry: './index.js',
    plugins: [new RemoveLogs()]
}