在 Webpack 的浩瀚宇宙中,Plugin(插件) 是最强大的扩展机制。如果说 Loader 只是负责转换文件的"翻译官",那么 Plugin 就是能控制 Webpack 整个构建生命周期的"指挥官"。
而要开发一个 Plugin,第一步就是搞懂 compiler.hooks。
一、 核心隐喻:流水线上的"传感器"
为了理解 Webpack 的构建过程,我们可以把它想象成一个超级工厂的流水线:
- Compiler (编译器) :就是这个工厂的主控电脑。它在 Webpack 启动时被创建,全局唯一,保存了所有的配置(Options, Loaders, Plugins)。
- Hooks (钩子) :流水线上的一个个关键节点。
- Tapable:Webpack 内部的事件流机制。
当我们开发插件时,实际上就是在这些节点上安装"传感器"或"机械臂"。当流水线运行到某个节点(比如"准备写入文件"时),就会触发我们的逻辑,让我们有机会修改数据、增加文件或监控状态。
二、 Compiler Hooks 生命周期全景图
compiler.hooks 包含了几十个钩子,我们不需要全部背下来。按照构建流程,我们将最核心的钩子分为四个阶段:
1. 初始化阶段 (Initialization)
当 Webpack 读取配置并初始化插件时触发。
-
entryOption- 类型:SyncBailHook
- 作用 :在处理
entry配置项之后触发。如果你想动态修改入口配置,就在这里动手。
-
afterPlugins- 类型:SyncHook
- 作用 :所有插件的
apply方法执行完之后。适合做插件之间的联动检查。
2. 编译构建阶段 (Running & Building)
这是工厂开始运转,处理模块依赖的核心阶段。
-
run- 类型:AsyncSeriesHook
- 作用:构建真正开始前的"预热"阶段。可以用于清理缓存或读取外部记录。
-
compile- 类型:SyncHook
- 作用:告诉大家"我要开始创建编译对象了"。
-
compilation(⭐ 核心中的核心)- 类型:SyncHook
- 作用 :
compilation对象创建完成。 - 详解 :这是绝大多数插件的入口。
compilation代表**"仅仅这一次构建"**的资源集合。在这里,你可以通过访问compilation.hooks进一步深入到模块(Module)和代码块(Chunk)的处理细节中。
-
make- 类型:AsyncParallelHook
- 作用:编译过程正式启动,开始从 Entry 递归分析依赖。这是添加额外文件或模块的好时机。
3. 输出阶段 (Output)
资源已经处理完毕,准备打包生成的阶段。
-
emit(⭐ 最常用)- 类型:AsyncSeriesHook
- 作用 :资源已经转换完成,放在内存里,但还没写入磁盘。
- 实战 :这是修改最终输出文件的最后机会。比如:生成
file-list.md、压缩图片、添加版权头信息等。
-
afterEmit- 类型:AsyncSeriesHook
- 作用:文件已经写入磁盘之后。常用于清理临时文件。
4. 结束阶段 (Termination)
-
done- 类型:AsyncSeriesHook
- 作用:构建完成(无论成功还是有警告)。常用于打印统计信息(Stats)或发送构建完成通知。
-
failed- 类型:SyncHook
- 作用:构建因错误而失败。用于错误上报。
三、 实战:如何"挂载"你的逻辑?
在插件类的 apply 方法中,我们使用 .tap 系列方法来注册钩子。根据钩子的类型(同步或异步),有不同的写法。
1. 同步钩子 (SyncHook)
使用 .tap()。逻辑执行完,流水线继续。
javascript
class MyStartPlugin {
apply(compiler) {
// 参数1: 插件名称(建议写类名),参数2: 回调
compiler.hooks.compile.tap('MyStartPlugin', (params) => {
console.log('>>> 编译器启动,准备干活!');
});
}
}
2. 异步钩子 (AsyncHook)
类似于 emit 这种涉及文件 I/O 的操作,通常是异步的。你有两种选择:
方式 A:回调风格 (tapAsync)
注意:千万别忘了调用 callback(),否则构建会卡死!
javascript
class FileListPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// 1. 读取当前生成的所有资源名称
const fileNames = Object.keys(compilation.assets);
const content = `Generated files:\n ${fileNames.join('\n')}`;
// 2. 向输出列表中添加一个新的文件
compilation.assets['file-list.txt'] = {
source: () => content,
size: () => content.length
};
// 3. 通知 Webpack 继续
console.log('文件列表已生成');
callback();
});
}
}
方式 B:Promise 风格 (tapPromise)
返回一个 Promise,Webpack 会等待它 resolve。
javascript
compiler.hooks.emit.tapPromise('MyPromisePlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('异步任务完成');
resolve();
}, 1000);
});
});
四、 避坑指南:Compiler vs Compilation
初学者最大的困惑往往是: "我到底该用 compiler.hooks 还是 compilation.hooks?"
| 对象 | 作用域 | 关注点 | 举例 |
|---|---|---|---|
| Compiler | 宏观 (整个生命周期) | 构建的起止、配置、环境 | "什么时候开始跑?"、"什么时候写完文件?" |
| Compilation | 微观 (单次构建) | 具体的模块、依赖、代码生成 | "如何压缩这个 JS?"、"这个 CSS 的 Hash 怎么算?" |
使用原则:
一般我们通过 compiler.hooks.compilation 作为入口,拿到 compilation 实例,然后再去注册 compilation.hooks 来处理具体的资源。
javascript
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
// 现在你可以操作具体的模块钩子了
compilation.hooks.optimizeChunkAssets.tap(...)
});
总结
掌握 compiler.hooks 是通往 Webpack 高级玩法的必经之路。
- EntryOption/Run: 配置与预处理。
- Compilation: 深入模块处理的入口。
- Emit: 修改最终产物的最后机会(最常用)。
- Done: 统计与通知。
理解了这条流水线,你就拥有了随意定制前端构建流程的能力。 Happy Coding!