写自己的插件(二):实战——注入 Banner 与生成版本清单
本节目标
- 写出一个「支持 options 参数」的可配置插件
- 给每个 bundle 顶部注入版权 banner
- 生成一份
manifest.json列出所有输出文件与版本号
真实插件几乎都要「可配置」。我们把上一节升级:写一个 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 与开发服务器。