Webpack 扩展与构建生命周期
从 Compiler、Compilation、Module、Chunk、Bundle 到 Loader/Plugin、分包、压缩和可验证的自定义扩展。
Webpack 扩展与构建生命周期
1. 五个核心对象
| 对象 | 关注点 |
|---|---|
Compiler |
一次完整构建的全局编排器,持有配置和生命周期 hooks |
Compilation |
某次构建的模块图、依赖、chunk、assets 和中间状态 |
Module |
一个被解析/转换的源模块或资源模块 |
Chunk |
按入口、动态导入和分包规则组织的一组模块 |
Bundle/Asset |
最终写入磁盘的 JS、CSS、图片、字体等文件 |
可以把 Webpack 概括为:从 entry 出发解析依赖图,使用 loader 将模块转换成可分析内容,再由 plugin 参与生命周期,最后组装 chunk 和 assets 输出。
2. 构建主线
读取配置
-> 创建 Compiler
-> run/watch
-> 根据 entry addEntry
-> 解析依赖并创建 Module
-> loader 转换、递归构建 moduleGraph
-> seal:优化模块/Chunk、Tree Shaking、SplitChunks
-> 生成 assets
-> emit 写入文件系统
2.1 初始化
Webpack 读取 mode、entry、output、module.rules、resolve、externals、plugins 等配置,创建 Compiler。插件通常在 apply(compiler) 中注册全局 hooks。
2.2 构建模块图
从入口模块开始,解析 import/require/动态导入;匹配 loader 并按规则转换源码,再解析转换结果中的依赖,直到模块图闭合。解析失败、loader 输出非法或依赖循环处理不当都会在这一阶段暴露。
2.3 生成和输出
模块图完成后,Webpack 根据入口、异步边界和 splitChunks 组装 chunk/chunk group,再生成 assets。生成阶段是修改输出内容的最后窗口之一;之后通过文件系统写入并触发完成 hooks。生产环境应保留构建 stats、release 和 source map 关联。
3. 常用 hooks 与选择
| Hook | 时机 | 典型用途 |
|---|---|---|
compiler.hooks.compilation |
创建 Compilation 后 | 订阅本次模块图/资源生命周期 |
compiler.hooks.make |
正式构建入口时 | 添加入口或异步构建任务 |
compilation.hooks.optimizeChunks |
chunk 集合形成后 | 自定义 chunk 优化或统计 |
compiler.hooks.emit |
产物即将写出(旧式 API) | 读取/修改 assets;新项目优先 processAssets |
compilation.hooks.processAssets |
资源处理阶段 | 按指定 stage 生成/修改 assets |
compiler.hooks.done |
构建完成 | 输出 stats、记录构建指标 |
Webpack 版本会调整 hooks 和 asset API。面试中可讲清时机和数据边界,实际代码应查目标版本类型声明,避免照抄旧版 compilation.assets[name] = ...。
4. Loader:模块级转换函数
Loader 接收资源内容,返回转换后的内容或通过 callback 异步返回:
// replace-author-loader.cjs
const { getOptions } = require('loader-utils')
module.exports = function replaceAuthor(source, inputSourceMap) {
const callback = this.async()
const options = getOptions(this) || {}
const author = options.author ?? 'anonymous'
const output = source.replaceAll('{{author}}', author)
// 替换可能改变字符长度,原 map 的列位置不再可靠;应生成新 map。
// 这里选择明确丢弃旧 map,生产 loader 应使用 source-map 工具重建映射。
callback(null, output, null)
}
Loader 需要考虑:
this.resourcePath、this.getOptions()和this.addDependency()等上下文;- 同步返回还是
this.async()callback; - loader 链从右到左/从下到上的执行顺序(以目标版本文档为准);
- 输入类型(字符串/Buffer)、source map、错误位置和缓存标识;
- 只处理
include范围内的文件,避免扫描node_modules或生成目录; - 配置可序列化,确保持久化缓存可命中。
Loader 只负责“一个模块如何变换”,不应偷偷修改整个构建或写任意目录。涉及全局资源、入口和输出清单时,应考虑 Plugin。
5. Plugin:流程级扩展对象
Plugin 通常是带 apply 方法的对象:
class BuildNoticePlugin {
constructor(options = {}) {
this.filename = options.filename ?? 'build-notice.txt'
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('BuildNoticePlugin', (compilation) => {
const { RawSource } = compiler.webpack.sources
compilation.hooks.processAssets.tap(
{
name: 'BuildNoticePlugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
() => {
const names = Object.keys(compilation.assets).sort()
compilation.emitAsset(
this.filename,
new RawSource(`Generated assets:\n${names.join('\n')}\n`),
)
},
)
})
}
}
module.exports = BuildNoticePlugin
这个插件展示了“需求 -> hook -> 输入/输出 -> 错误处理 -> 测试”的最小闭环。实际项目还应处理重复生成、资产命名冲突、source map、watch 模式和构建取消。
6. Loader 与 Plugin 的区别
| 维度 | Loader | Plugin |
|---|---|---|
| 抽象 | 函数 | 对象/类的 apply |
| 范围 | 单个模块或资源 | Compiler/Compilation 全流程 |
| 输入输出 | source -> source/AST/asset | hooks、module graph、chunks、assets |
| 例子 | TS、JSX、Sass、Markdown 转换 | HTML、清单、压缩、分包、分析、HMR |
| 典型测试 | 输入 fixture 到输出 fixture | hook 时机、stats 和 assets |
简单记忆:Loader 改“文件内容”,Plugin 改“构建过程”。一个功能可能同时需要二者,但边界要明确。
7. 分包、Tree Shaking 和资源优化
7.1 splitChunks
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
},
},
},
runtimeChunk: 'single',
}
按路由动态导入、公共依赖、更新频率和缓存收益切分;不要只追求 chunk 数量或把所有第三方打进一个永不更新的大包。过细分包会增加请求、解析和调度成本,需结合 Network 和 RUM 验证。
7.2 Tree Shaking
Tree Shaking 依赖 ESM 的静态结构、生产模式、side effects 声明和代码写法。CommonJS 动态导出、错误的 sideEffects: false、模块顶层注册和聚合入口都可能削弱或破坏摇树。CSS、polyfill 和全局注册应明确列为副作用。
7.3 CSS 提取与压缩
MiniCssExtractPlugin 等工具可将 CSS 从 JS 中提取成独立文件,便于并行下载和缓存;按路由拆分时要处理 FOUC、关键 CSS 和样式顺序。css-minimizer-webpack-plugin/cssnano 负责压缩,但删除未使用规则前要覆盖动态 class、主题和 SSR。
7.4 Terser 与 source map
Terser 可压缩、折叠和删除不可达代码;是否删除 console 应由日志策略决定。生产 source map 不应公开源码,上传到受控错误平台,并以 release/构建 hash 确保映射一致。
7.5 Dll 与现代替代
DllPlugin 通过预构建稳定第三方依赖减少重复构建,在历史 Webpack 工程中可能有价值;Webpack 5 filesystem cache、Vite 依赖预构建和稳定 lockfile 通常是更易维护的优先方案。选择要以 profile 和团队维护成本为证据,不要因为名词“高级”就引入。
8. 构建速度诊断
- 记录冷启动、增量构建、CI 构建和产物压缩分别耗时;
- 查看 loader/plugin timing、profile 和缓存命中率;
- 收窄
include/exclude,启用 filesystem cache,减少无关插件; - 仅在 CPU/内存足够且 profile 证明有效时并行 transform/压缩;
- 检查 source map、依赖预构建和 Monorepo 软链包是否造成额外处理;
- 用相同 Node、锁文件、机器和配置复测,并把结果纳入 CI 趋势。
构建快不等于产物好:类型检查、Lint、测试、source map、可重复性和错误定位仍是交付质量的一部分。
9. HMR 链路
文件变更 -> watcher -> 增量编译 -> 生成 hash/模块更新
-> dev server/WebSocket 推送 -> 客户端接受并替换 -> 状态保留或 full reload
若模块没有可接受边界、状态不可恢复或更新冒泡到入口,HMR 会退化为整页刷新。排查时同时看终端编译日志、浏览器 HMR 消息、模块边界和插件转换结果。React/Vue 的 Fast Refresh/SFC HMR 由框架插件提供额外规则,并非 Webpack 单独保证。
10. 自定义扩展测试
Loader
- 输入多种编码、空文件和异常语法;
- 验证同步/异步 callback 只调用一次;
- 检查 source map、选项和缓存依赖;
- 确认不处理 include 外文件。
Plugin
- 用最小 fixture 运行真实 Webpack 编译;
- 断言 hook 时机、生成 assets、文件内容和命名冲突;
- 测试 watch/重复编译/错误中止;
- 检查升级 Webpack 后类型和 API 变化。
11. 面试追问速答
Q: Webpack 的 Module、Chunk、Bundle 怎么区分?
A: Module 是源码或资源模块;Chunk 是按入口/异步边界/分包规则组织的一组模块;Bundle/Asset 是最终写到磁盘的文件。一个 chunk 可能生成一个或多个 asset。
Q: Loader 为什么不能替代 Plugin?
A: Loader 作用于单个模块输入输出,Plugin 通过 compiler/compilation hooks 参与全局流程,才能访问入口、chunk、资产和构建统计。把流程级逻辑塞进 loader 会破坏缓存和职责边界。
Q: 自定义 Plugin 怎么选择 hook?
A: 先明确需求发生在初始化、模块构建、chunk 优化、asset 处理还是完成统计,再选择对应 compiler/compilation hook,并查目标 Webpack 版本的 stage 和 API。不要只凭 emit 旧示例。
Q: 如何证明优化有效?
A: 用固定环境的 profile 和产物报告对比构建耗时、入口/路由传输字节、解析执行、缓存命中率,再用真实页面的 LCP/INP/错误率验证;如果只改善本地启动,不应宣称线上收益。
12. 资料来源与现有专题
本篇整理 前端架构师工程化思维与编译原理详解.pdf 的 Compiler/Compilation/Module/Chunk/Bundle、初始化/构建/生成阶段、hooks、自定义 Loader/Plugin、SplitChunks、Tree Shaking、Dll、CSS 提取和 Terser 内容,并采用 Webpack 5 的 asset API 做校正。相关专题:Webpack 构建流程、Loader 与 Plugin、构建体积与性能治理。