写自己的插件(二):实战——注入 Banner 与生成版本清单

本节目标

真实插件几乎都要「可配置」。我们把上一节升级:写一个 ReportPlugin,接收 options(如版本号、作者),做两件事——① 在 JS bundle 顶部加 banner;② 生成 manifest.json。

// report-plugin.js
class ReportPlugin {
  // options 由 new ReportPlugin({...}) 传入
  constructor(options = {}) {
    this.version = options.version || '0.0.0'
    this.author = options.author || 'anonymous'
  }

  apply(compiler) {
    const { version, author } = this
    compiler.hooks.thisCompilation.tap('ReportPlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'ReportPlugin',
          stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
        },
        () => {
          // ① 给每个 .js 文件顶部注入 banner
          for (const filename of Object.keys(compilation.assets)) {
            if (!filename.endsWith('.js')) continue
            const asset = compilation.assets[filename]
            const raw = asset.source().toString()
            const banner = `/* ${author} · v${version} · ${new Date().toISOString()} */\n`
            // 用新字符串内容覆盖原 asset
            compilation.assets[filename] = {
              source: () => banner + raw,
              size: () => banner.length + raw.length,
            }
          }

          // ② 生成 manifest.json:列出所有产物
          const manifest = {
            version,
            author,
            generatedAt: new Date().toISOString(),
            files: Object.keys(compilation.assets),
          }
          const json = JSON.stringify(manifest, null, 2)
          compilation.assets['manifest.json'] = {
            source: () => json,
            size: () => json.length,
          }
        }
      )
    })
  }
}

module.exports = ReportPlugin

配置里传入参数:

// webpack.config.js
const ReportPlugin = require('./report-plugin.js')
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js', path: __dirname + '/dist', clean: true },
  plugins: [
    new ReportPlugin({ version: '1.2.0', author: 'laowen' }),
  ],
  mode: 'production',
}

跑完你会看到:bundle.js 第一行为 /* laowen · v1.2.0 · ... */,dist/manifest.json 列出了全部产出文件。这只是一个起点——你完全可以把「注入 banner」换成「注入环境变量」「按文件名加 hash 校验」「统计每个 chunk 体积」,思路完全一致:在 processAssets 读 asset.source()、改完写回 assets[文件名]。

名词解释

Options(插件参数):通过构造函数 new Plugin({...}) 传入的配置对象,让同一个插件在不同项目里表现不同,是插件「可复用」的关键。

Banner(版权头):加到文件顶部的一行注释,常用来声明版权、版本、构建时间。Webpack 官方也有 BannerPlugin,这里我们亲手实现一遍以理解原理。

覆盖 Asset(source/asset 替换):通过 asset.source().toString() 读出内容、拼接新内容、再写回 compilation.assets[文件名],是「修改已有文件」的标准手法。

Manifest(清单):描述构建产物元信息的文件(本例列出所有输出文件名 + 版本),常用于做「产物指纹」「发布审计」。

课后练习

练习 1:PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE 这个 stage 影响什么?

答案:它决定「你的插件在 processAssets 队列里的执行先后」。选 OPTIMIZE_INLINE 阶段,表示「在优化内联附近执行」,能保证 banner 注入发生在压缩之前/之间,顺序可预期。多个插件依赖同一资源时,stage 是唯一的排序依据。

练习 2:如果我想「统计每个 chunk 的体积并写进 manifest」,逻辑加在哪?

答案:同样在 processAssets 回调里,遍历 compilation.assets,对每个 .js 用 asset.size() 取体积,汇总进 manifest 对象再写入 manifest.json。核心是「asset 提供 source()/size(),你只管读和写」。

总结

你现在拥有了「写生产级插件」的全部骨架:构造函数收 options → apply 里挂 processAssets → 读 asset.source()、改完写回 assets。Banner 注入与 manifest 生成只是两个范例,这套模式能延伸出注入 env、体积分析、产物校验无数玩法。我的建议:写插件前先问「我要干预哪个阶段、改 assets 还是加 assets」——答案清晰了,代码自然就那三行。下一节跳出插件,回到工程全局:代码分割、Tree Shaking 与开发服务器。