Plugin
Webpack 插件在构建生命周期钩子上扩展能力(修改资源、生成 HTML、上传 CDN 等)。背景见 工作原理;与 Loader 分工:Loader 转换单文件,插件面向整体构建。
一、插件基本架构
每个插件都是一个具有 apply(compiler) 方法的类或对象。在 apply 中通过 compiler.hooks 注册钩子回调。
class MyPlugin {
apply(compiler) {
// 同步钩子使用 tap
compiler.hooks.compile.tap("MyPlugin", (params) => {
console.log("开始编译");
});
// 异步串行钩子使用 tapAsync(调用 callback)
compiler.hooks.emit.tapAsync("MyPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("发射文件前");
callback();
}, 1000);
});
// 异步并行钩子使用 tapPromise(返回 Promise)
compiler.hooks.run.tapPromise("MyPlugin", (compiler) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("运行开始");
resolve();
}, 500);
});
});
}
}
钩子类型决定注册方式:
SyncHook:只能用tapSyncBailHook:只能用tap,返回非undefined则中断AsyncParallelHook:可用tap/tapAsync/tapPromiseAsyncSeriesHook:同上,但串行执行
二、Compiler 与 Compilation 核心属性
1. compiler 对象
代表整个 Webpack 实例,包含了配置、文件系统、插件集合等。
| 属性 | 说明 | 常用场景 |
|---|---|---|
compiler.options | 最终的合并配置 | 读取用户配置项 |
compiler.inputFileSystem | 读取文件的文件系统(默认 fs) | 手动读取文件 |
compiler.outputFileSystem | 输出文件的文件系统 | 写入自定义文件 |
compiler.hooks | 所有生命周期钩子 | 注册插件逻辑 |
compiler.context | 项目根目录绝对路径 | 解析相对路径 |
2. compilation 对象
代表一次完整的资源构建过程,包含当前打包的所有模块、chunk、生成资源等。
| 属性 | 说明 | 常用场景 |
|---|---|---|
compilation.modules | 所有模块的集合(Set) | 遍历所有模块,筛选特定模块 |
compilation.chunks | 所有 chunk 的集合 | 修改 chunk 名称或内容 |
compilation.assets | 生成的文件资源对象(key: 文件名, value: Source) | 修改、添加、删除输出资源 |
compilation.errors / warnings | 错误/警告数组 | 收集并上报错误 |
compilation.inputFileSystem | 同 compiler.inputFileSystem | 读取文件 |
compilation.getStats() | 生成 stats 对象 | 输出构建统计信息 |
修改 assets 示例:
compiler.hooks.emit.tapAsync("MyPlugin", (compilation, callback) => {
// 添加一个新文件
compilation.assets["my-version.json"] = {
source: () => JSON.stringify({ version: "1.0.0" }),
size: () => 20,
};
// 或使用 webpack-sources
const { RawSource } = require("webpack-sources");
compilation.assets["info.txt"] = new RawSource("Build completed");
callback();
});
三、常见自定义插件实现
1. 生成额外文件(如 manifest.json、版本信息)
class GenerateManifestPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync("GenerateManifestPlugin", (compilation, callback) => {
const manifest = {
timestamp: Date.now(),
files: Object.keys(compilation.assets),
};
const json = JSON.stringify(manifest, null, 2);
compilation.assets["manifest.json"] = {
source: () => json,
size: () => json.length,
};
callback();
});
}
}
2. 修改最终输出的 HTML(配合 HtmlWebpackPlugin)
class HtmlModifyPlugin {
apply(compiler) {
compiler.hooks.compilation.tap("HtmlModifyPlugin", (compilation) => {
const HtmlWebpackPlugin = require("html-webpack-plugin");
const hooks = HtmlWebpackPlugin.getHooks(compilation);
hooks.beforeEmit.tapAsync("HtmlModifyPlugin", (data, cb) => {
data.html = data.html.replace(/<body>/, '<body><div id="custom">Inject</div>');
cb(null, data);
});
});
}
}
3. 打包后自动上传 CDN
class UploadToCDNPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tapPromise("UploadToCDNPlugin", async (compilation) => {
const assets = compilation.assets;
for (const [filename, source] of Object.entries(assets)) {
const content = source.source();
// 调用云存储 SDK 上传
await uploadToCDN(filename, content);
}
});
}
}
4. 移除指定注释内容
class RemoveCommentsPlugin {
apply(compiler) {
compiler.hooks.compilation.tap("RemoveCommentsPlugin", (compilation) => {
// 使用 TerserPlugin 的 hooks 或直接修改 assets
compilation.hooks.processAssets.tap(
{
name: "RemoveCommentsPlugin",
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
},
(assets) => {
for (const [filename, source] of Object.entries(assets)) {
const content = source.source().toString();
const cleaned = content.replace(/\/\*.*?\*\//gs, "").replace(/\/\/.*$/gm, "");
// 更新 assets
compilation.updateAsset(filename, new RawSource(cleaned));
}
},
);
});
}
}
四、使用 webpack-sources 处理代码字符串
Webpack 内部使用 webpack-sources 库来表示源代码,支持多种 Source 类型,可以进行拼接、修改、缓存等操作。
| Source 类型 | 用途 |
|---|---|
RawSource | 原始字符串内容 |
ConcatSource | 拼接多个 Source |
CachedSource | 缓存内部 Source 结果,避免重复计算 |
ReplaceSource | 在原始内容中进行替换操作(保留 source map) |
示例:在生成的 JS 文件头部添加注释
const { RawSource, ConcatSource } = require("webpack-sources");
class AddHeaderPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync("AddHeaderPlugin", (compilation, callback) => {
const header = new RawSource("/* This file generated by Webpack */\n");
for (const filename in compilation.assets) {
if (filename.endsWith(".js")) {
const original = compilation.assets[filename];
const combined = new ConcatSource(header, original);
compilation.assets[filename] = combined;
}
}
callback();
});
}
}
替换内容(保留 source map):
const { ReplaceSource } = require("webpack-sources");
// 在某个 loader 内,也可在插件中使用
const replaceSource = new ReplaceSource(originalSource);
replaceSource.replace(0, 10, "new content");
replaceSource.insert(15, "inserted");
const result = replaceSource.source(); // String
result.sourceAndMap; // { source, map }
五、Node 调试 Webpack 插件
使用 Chrome DevTools 可以断点调试插件代码。
1. 方法一:命令行启动
node --inspect-brk ./node_modules/webpack/bin/webpack.js --config webpack.config.js
--inspect-brk:在第一行代码处断点,等待调试器连接。- 打开 Chrome 浏览器,访问
chrome://inspect,点击「Open dedicated DevTools for Node」。
2. 方法二:VS Code 调试配置
在 .vscode/launch.json 中添加:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Webpack",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "node",
"args": [
"--inspect-brk",
"./node_modules/webpack/bin/webpack.js",
"--config",
"webpack.config.js"
]
}
]
}
然后在插件代码中直接打断点即可。
3. 调试技巧
- 在插件的
apply方法内设置debugger语句。 - 使用
console.log(compiler.hooks)查看可用的钩子和参数。 - 通过
compiler.options和compilation.assets检查当前状态。
总结
| 方面 | 要点 |
|---|---|
| 基本架构 | apply(compiler) + 钩子注册(tap/tapAsync/tapPromise) |
| Compiler | 全局单例,包含配置、文件系统、生命周期钩子 |
| Compilation | 每次构建的上下文,包含 modules、chunks、assets(关键修改点) |
| 常见插件 | 生成额外文件、修改 HTML、上传 CDN、移除注释等 |
| webpack-sources | RawSource、ConcatSource、ReplaceSource 处理代码字符串 |
| 调试 | --inspect-brk + Chrome DevTools 或 VS Code 启动配置 |
开发高级插件时,注意钩子的执行时机(emit 适合修改最终资源,processAssets 是 Webpack 5 推荐的资源处理阶段),并合理使用 webpack-sources 保证 source map 支持。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Plugin API | 官方 |
| 编写 Plugin | 指南 |
相关文章
工作原理
Webpack 是静态模块打包器:从入口递归解析依赖,经 Loader 转换后输出一个或多个 bundle。与 Vite 对比见 Webpack 与 Vite。
产物优化
Webpack 通过代码分割、Tree Shaking、压缩与资源模块等减少体积、改善缓存。原理背景见 工作原理。
企业级实践
大型前端项目在 Webpack 场景下的多环境拆分、Docker 构建缓存、自定义 CLI 与微前端(Module Federation / single-spa)等实践。入门见 工程化概览。
Loader
Loader 在 Webpack 构建链中把源文件转为可打包的 JS 模块(如 TS、SCSS、Vue SFC)。见 工作原理。
进阶配置
Webpack 进阶场景:多页面(MPA)、devServer、环境变量注入、Source Map、Module Federation 等。基础见 工作原理。
插件开发
Vite 插件兼容 Rollup 插件,并扩展 config、configureServer、transformIndexHtml 等钩子。原理见 工作原理。