如何编写Webpack插件

223 阅读4分钟

 一般应届生面试时,面试官经常问你是否手写过一个webpack插件?你的回答是什么呢?今天让小前带你来探索下webpack的插件是如何编写的,保证你看完,不禁感叹,“原来webpack插件编写也就是那么回事”。话不多说,开干!

Tapable

Tapable 主要控制webpack钩子函数的发布与订阅,可以把它理解为类似 nodejs 的EventEmitter 的库。webpack的本质就是一系列插件运行

Tapable库暴露出来很多钩子类,这些类可以给插件创建钩子

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");

最好的方法是在 类中通过hooks属性将所有钩子暴露出去,webpack常用的钩子有runcompilemakeemitdone等等

class Compilation {
    constructor() {
        this.hooks = {
            // ...
            run: new AsyncSeriesHook(),
            compile: new SyncHook(),
            make: new AsyncParallelHook(),
            emit: new AsyncSeriesHook(),
            done: new AsyncSeriesHook(),
            // ...
        };
    }

}

当需要在compile这个钩子添加插件时,可以通过钩子的tap方法绑定

// 第一个参数是标识符,用来标识插件名称
Compilation.hooks.compile.tap("myPluginName", () => {
    // 做一些关于插件的骚操作处理
});

在同步钩子中,tap是唯一的绑定方法;而异步钩子可以通过tapAsync或者tapPromise绑定。小前本次编写的插件将会基于异步钩子emit编写,所以会使用tabAsync,用法与tap一样。想了解更多可以查看webpack关于 Tapale 的介绍

Plugin用途

刚好最近项目调试用了一堆console.log语句,每次都要手动清除,实在费时间,今天既然遇上了 Plugin,那么我们就实现一个Clean-Console-Plugin插件,在打包的时候将所有console.log语句干掉

备注:该功能目前已经有成熟的插件可以使用,当前主要是通过该功能来演示如何编写一个Plugin,生产环境请勿使用。更好的做法是通过编写一个loader,在遍历AST时进行操作删除console.log语句,也可以使用uglifyjs-webpack-plugin

Plugin实现

Plugin编写时会接触到webapck出来的两个参数:

Compiler: 该对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件的回调函数里将收到此 compiler 对象的引用作为参数。可以使用它来访问 webpack 的主环境

Compilation:如果说compiler时对整个编译过程起效果,那么compilation时对某一个阶段的编译结果进行操作的

我们希望是在webpack将asset输出到output目录之前执行,所以我们将会在emit这个钩子里编写插件。了解更多钩子可以查看webpack自身的 事件钩子

在使用插件时,一般时通过new **Plugin()的方式提供插件,同时传入参数,所以我们编写插件的时候将通过类来编写,并且在构造函数里初始化提供的参数

class HelloWebpackPlugin {
    constructor(options) {
        this.options = options;
    }
    
    apply(compiler) {
        compiler.hooks.emit.tapAsync('HelloWebpackPlugin', (compilation, callback) => {
            // 实现插件逻辑代码
            this.removeLogs(compilation)
            callback();
        })
    }
}

module.exports = HelloWebpackPlugin;

类需要提供apply方法,该方法将在安装插件时,会被webpakc的compiler调用一次。该方法接收compiler对象的引用

apply方法中通过compiler可以拿到所有钩子,通过emit这个异步钩子绑定调用tapAsync方法绑定插件,该方法有两个参数compilation和回调函数,当通过tabAsync方法来绑定插件时,必须调用最后一个参数callback

removeLogs(compilation) {
    Object.keys(compilation.assets).forEach(filename => {
        const sourceText = compilation.assets[filename].source();
        // 内容进行正则替换删除console.log
    })
}

compilation上的assets对象的key是当前输出文件的文件路径,value是一个对象,该对象上面source方法可以获取到当前文件的内容。获取到内容后需要做的就是通过正则内容替换,最后再更新assets即可

removeLogs(compilation) {
    Object.keys(compilation.assets).forEach(filename => {
        const sourceText = compilation.assets[filename].source();
        const rgx = /console.log\(['|"](.*?)['|"]\)/;
        const newData = sourceText.replace(rgx, "");
        compilation.assets[filename] = {
            source: function() {
                return newData;
            },
            size: function() {
                return newData.length;
            }
        }
    })
}

更新assets的时候需要一个对象,包含sourcesize两个方法,source返回新文件内容,size返回新文件字符长度

// webpack配置文件
const webpack = require("webpack");
const path = require('path');
const HtlmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const HelloWebpackPlugin = require("./src/plugins/hello-webpack-plugin");

module.exports = {
    mode: 'production',
    entry: {
        index: './src/index.js',
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new HtlmlWebpackPlugin(),
        new CleanWebpackPlugin(),
        new HelloWebpackPlugin()
    ]
}

编写示例:导出一个函数绑定到index文件中某个元素的onclick事件上

export function hello() {
    alert('欢迎点击');
    console.log('hello my plugin');
}

最后通过运行npx webpack命令打包,可以发现打包输出的文件报错

1647097098(1).png

因为是生产模式打包会将代码压缩成一行,上下两行的语句通过,连接,由于console.log("*")的内容删除了但是,还在所以报错,这里的正则需要hack下将,一起删除,正则替换成/,console.log\(['|"](.*?)['|"]\)/

完整的Plugin代码:

class HelloWebpackPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('HelloWebpackPlugin', (compilation, callback) => {
            console.log('assets', compilation.assets);
            Object.keys(compilation.assets).forEach(filename => {
                const sourceText = compilation.assets[filename].source();
                const rgx = /,console.log\(['|"](.*?)['|"]\)/;
                const newData = sourceText.replace(rgx, "");
                compilation.assets[filename] = {
                    source: function() {
                        return newData;
                    },
                    size: function() {
                        return newData.length;
                    }
                }
            })
            callback();
        })
    }
}

module.exports = HelloWebpackPlugin;

关注公众号《小前日记 》获取源码

IMG_5155.JPG