写自己的插件(一):注册钩子并在 emit 阶段生成文件

本节目标

现在动手。一个 Webpack 插件,就是一个含有 apply(compiler) 方法的类(或对象)。我们在「即将输出文件」的钩子上,往 compilation.assets 里追加一个文件,就完成了「生成文件」。

下面这个插件会在每次构建后,自动生成一个 build-info.txt,记录构建时间与入口数量:

// my-first-plugin.js
class BuildInfoPlugin {
  apply(compiler) {
    // 推荐用 thisCompilation + processAssets(webpack5 官方推荐写法)
    compiler.hooks.thisCompilation.tap('BuildInfoPlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'BuildInfoPlugin',
          // 声明本插件在「优化资产之后、 Seal 之前」执行
          stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT,
        },
        () => {
          const info = [
            '构建时间: ' + new Date().toLocaleString(),
            '产物文件数: ' + Object.keys(compilation.assets).length,
          ].join('\n')

          // 往最终输出清单里加一个文件
          compilation.assets['build-info.txt'] = {
            source: () => info,       // 文件内容
            size: () => info.length,  // 文件大小(字节)
          }
        }
      )
    })
  }
}

module.exports = BuildInfoPlugin

在配置里用起来:

// webpack.config.js
const BuildInfoPlugin = require('./my-first-plugin.js')
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js', path: __dirname + '/dist', clean: true },
  plugins: [new BuildInfoPlugin()],
  mode: 'development',
}

跑 npx webpack,dist/ 里除了 bundle.js 还会多一个 build-info.txt。关键点:compilation.assets 就是「最终要写到磁盘的资源清单」,你往里塞一个 { source, size } 对象,它就会被输出成文件。就这么简单。

兼容性说明:旧教程常写 compiler.hooks.emit.tap(...) 直接改 compilation.assets,webpack4/5 都能跑;但 webpack5 官方更推荐 processAssets(在资源已生成、尚未封存时操作,更安全可控)。本例用 processAssets,是「现代正确姿势」。

名词解释

apply(compiler):插件必须实现的方法,Webpack 在初始化插件时调用它,把 compiler 传进来,你在这里挂钩子。

processAssets:compilation 上的钩子,时机是「所有资产已生成、准备封存输出前」,是往产物里加/改文件的最佳干预点。

compilation.assets:本次构建的「最终资源清单」对象,key 是文件名,value 是带 source()/size() 的资源对象。改它就是改产物。

PROCESS_ASSETS_STAGE_*:webpack5 给 processAssets 划分的执行阶段常量(REPORT / OPTIMIZE / SUMMARIZE 等),用来控制插件间的先后。

课后练习

练习 1:为什么「往 compilation.assets 加对象」就能生成文件,而不是直接写磁盘?

答案:Webpack 是「先收集资产、最后统一写盘」的模型。assets 是输出前的「清单」,Webpack 写完所有钩子后,才按这份清单把每个资源的 source() 写到磁盘。你改清单而非直接 IO,既统一又可控(还能被后续插件再加工)。

练习 2:emit 钩子和 processAssets 都能加文件,选哪个?

答案:优先 processAssets(webpack5 官方推荐,处在「资产已就绪」阶段,语义清晰、可声明 stage 控制顺序);emit 也可用且更「老派」,但它在更靠后的时机,部分资产细节已定型,灵活度略低。

总结

你的第一个插件已经能「自动生成文件」了。核心套路三步走:① 写个带 apply(compiler) 的类;② 在 processAssets 钩子上 .tap;③ 往 compilation.assets 塞 { source, size }。记住 assets 即「输出清单」这个心智模型,后面所有「加文件/改文件」的魔法都源于此。下一节把它升级成两个实战插件:注入版权 banner、生成依赖清单。