插件机制:Tapable、compiler 与 compilation 的生命周期

本节目标

前面 loader 管「单个文件怎么转译」,而 plugin(插件) 管的是「整个构建过程的某个时机做什么事」——比如:打包后自动生成 HTML、把 CSS 抽出去、注入版权 banner、生成资源清单。

Webpack 内部用 Tapable 这套「钩子(Hook)系统」来调度。你可以把它理解成一根时间轴上的许多「事件节点」,每个节点上可以「挂」你的逻辑。两个核心对象:

一个最简单的「使用插件」例子——自动生成引入 bundle 的 HTML:

# npm i -D html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js', path: __dirname + '/dist', clean: true },
  plugins: [
    new HtmlWebpackPlugin({ title: '我的应用' }), // 自动生成 dist/index.html 并注入 bundle
  ],
  mode: 'development',
}

插件怎么「挂」到钩子上?看一个插件内部长这样(概念示意):

// 插件是一个有 apply(compiler) 方法的类/对象
class MyPlugin {
  apply(compiler) {
    // compiler.hooks.emit 是「即将输出文件前」这个时机
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      console.log('当前要输出的资源有:', Object.keys(compilation.assets))
    })
  }
}

compiler.hooks.emit.tap('名字', 回调) 就是「在 emit 这个钩子上,注册一个监听」。Tapable 提供多种钩子:tap(同步)、tapAsync(异步回调)、tapPromise(异步 Promise)。HtmlWebpackPlugin 正是在 emit/资源生成阶段往 compilation.assets 里加了一个 index.html。

名词解释

Plugin(插件):在构建生命周期的某个钩子上「做事」的扩展点。它能操作整体产物(加文件、改内容、统计),能力比 loader 大得多。

Tapable:Webpack 内部基于「发布-订阅」的钩子系统,提供了 SyncHook、AsyncSeriesHook 等钩子类型,是插件机制的底层引擎。

Hook(钩子):构建时间轴上的一个事件节点(如 compile、make、emit、done),插件通过 .tap() 把逻辑挂上去。

Compiler / Compilation:compiler 是构建全程唯一的「总控」;compilation 是单次编译的「快照」,包含本次的模块、chunk、资源(assets)。

课后练习

练习 1:loader 和 plugin 的根本区别一句话怎么讲?

答案:loader 是「文件级翻译官」,只负责把单个源文件转成 JS 模块;plugin 是「流程级参与者」,能在构建的任意钩子上影响整体产物(增删改文件、读统计)。前者管「内容」,后者管「流程」。

练习 2:为什么插件用 .tap() 而不是普通函数调用注册?

答案:因为 Tapable 钩子本质是「监听器列表」。.tap(name, fn) 把你的函数注册进该钩子的队列,到对应时机 Webpack 会依次调用所有监听器——这是标准的事件订阅模型。

总结

插件机制的灵魂就一句话:Webpack 把构建拆成一条带钩子的时间轴,插件在钩子上 .tap 自己的逻辑。compiler 管全局、compilation 管单次编译的 resources,HtmlWebpackPlugin 之类就是挂在 emit 阶段往产物里塞 HTML。理解到这一层,下一步写你自己的插件就只是「选对钩子 + 写对逻辑」。这正是下一节的实操主题。