一般应届生面试时,面试官经常问你是否手写过一个webpack插件?你的回答是什么呢?今天让小前带你来探索下webpack的插件是如何编写的,保证你看完,不禁感叹,“原来webpack插件编写也就是那么回事”。话不多说,开干!
Tapable
Tapable 主要控制webpack钩子函数的发布与订阅,可以把它理解为类似 nodejs 的EventEmitter 的库。webpack的本质就是一系列插件运行
Tapable库暴露出来很多钩子类,这些类可以给插件创建钩子
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
最好的方法是在 类中通过hooks属性将所有钩子暴露出去,webpack常用的钩子有run、compile、make、emit、done等等
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的时候需要一个对象,包含source和size两个方法,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命令打包,可以发现打包输出的文件报错
因为是生产模式打包会将代码压缩成一行,上下两行的语句通过,连接,由于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;