前言
首先我们先回忆一下Webpack
插件是如何使用的?下面是一份基础的Webpack
配置文件:
let htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: path.join(__dirname, 'src/index.js')
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new htmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html')
})
]
}
可以看到,插件都在plugins
中,并且这是一个数组,包含了每一个插件的实例化调用,可以给这个实例传一些参数,因此不难看出来,Webpack
插件就是一个类。
流程控制
而熟悉Webpack
,你一定知道,插件是在整个构建的生命周期中指定的时机来进行一些逻辑操作的,比如你希望在构建开始时控制台打印时间,在构建产物生成时再打印一次时间;或者是希望在构建过程中进行一些特定操作,这就需要了解Webpack
背后实现流程控制的原理。而Webpack
则是基于tapable
这个第三方库来实现的。
那tapable
是啥?怎么用?看一下这段代码:
xxx.tap
等这类写法很符合tapable风格,我们再来看看看下面的代码:
let { SyncHook } = require('tapable');
class MyPlugin{
constructor(){
this.hooks = new SyncHook();
}
// 注册事件
registryEvent(eventName, eventFn){
this.hooks.tap(eventName, () => {
eventFn();
});
}
// 执行事件
executeEvent(){
this.hooks.call();
}
}
let compiler = new MyHook();
compiler.registryEvent('事件1', () => {
console.log('事件1执行了');
});
complier.registryEvent('事件2', () => {
console.log('事件2执行了');
})
complier.executeEvent();
上述代码就是注册事件 -> 触发事件的一个流程,而回到Webpack
中,Webpack
也具有很多的流程阶段。
Webpack 的构建流程可以大致分为以下几个阶段:
- 初始化阶段:Webpack 准备编译环境。
- 编译阶段:Webpack 开始编译,读取配置和模块。
- 构建阶段:Webpack 处理模块依赖,生成依赖图谱。
- 输出阶段:Webpack 根据依赖图谱生成最终的输出文件。
- 完成阶段:Webpack 完成构建,输出结果。
在这些阶段中,Webpack 会触发多种钩子(Tapable 提供的 Hooks),插件可以通过这些钩子介入到构建流程中。
例如产出阶段,则是emit
代表了构建产物产物前,afterEmit
代表了构建产物产出后。Webpack
基于Tapable
实现了许多的钩子函数:
因此我们写插件,需要知道这段逻辑在Webpack
哪个阶段来执行,因此插件就是套在Tapable hook
里的一层逻辑。
就比如这是一个时间记录打印日志的插件,我在两个生命周期阶段打印了当前时间:
const { Compiler } = require('webpack');
class TimePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// 监听编译开始的钩子
compiler.hooks.run.tapAsync('TimePlugin', (compilation, callback) => {
console.log('编译开始时间:', new Date().toLocaleTimeString());
callback();
});
// 监听编译完成的钩子
compiler.hooks.done.tap('TimePlugin', (stats) => {
console.log('编译结束时间:', new Date().toLocaleTimeString());
});
}
}
module.exports = TimePlugin;
在这个示例中,TimePlugin
类监听了 run
和 done
两个钩子。run
钩子在每次编译开始时触发,而 done
钩子在编译结束时触发。tapAsync
方法用于异步钩子,允许在回调中处理异步逻辑。
怎么使用呢?也很简单,在webpack.config.js
中实例化下就好:
const TimePlugin = require('./TimePlugin');
module.exports = {
// Webpack 配置...
plugins: [
new TimePlugin({ /* 插件选项 */ })
]
};
那为什么逻辑需要编写在apply
方法中呢?看一下Webpack
源码解析plugins
的部分:
相当于做了一层约定,在编译阶段Webpack
会去遍历所有的插件,并且调用apply
方法,同时把Tapable
实例传进去,这样我们就可以在插件中调用所有的生命周期钩子函数了。
看到这里,你已经很熟悉了吧。再看一个Webpack5
的官方demo:
class FileListPlugin {
constructor(fileName1){
this.fileName = fileName1;
}
apply(compiler){
let self = this;
const { webpack } = compiler;
// Compilation 对象提供了对一些有用常量的访问。
const { Compilation } = webpack;
// RawSource 是其中一种 “源码”("sources") 类型,
// 用来在 compilation 中表示资源的源码
const { RawSource } = webpack.sources;
compiler.hooks.thisCompilation.tap('fileListDone', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: self.fileName,
// 用某个靠后的资源处理阶段,
// 确保所有资源已被插件添加到 compilation
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// "assets" 是一个包含 compilation 中所有资源(assets)的对象。
// 该对象的键是资源的路径,
// 值是文件的源码
// 生成 Markdown 文件的内容
const content = '# 这是一级标题';
// 向 compilation 添加新的资源,
// 这样 webpack 就会自动生成并输出到 output 目录
compilation.emitAsset(
self.fileName,
new RawSource(content)
);
}
)
});
}
}
module.exports = FileListPlugin;
代码中涉及到了2个生命周期,thisCompilation
代表了Webpack
开始编译的阶段;processAssets
代表了所有的静态资源已经生成完毕。
这个demo的主要作用是在Webpack
编译的靠后阶段,生成了一个markdown
文件,同时输出到了output
目录中。
结尾
看完希望你对于Wbepack
插件机制的了解能更上一层楼,评论区欢迎一起讨论。