Webpack构建流程深度解析
Webpack 构建是一次递归扫描加流水线加工:从 entry 遍历 import 建依赖图,Loader 转换模块,Plugin 通过 Tapable 钩子注入逻辑。 流程围绕 Compiler(全局管家)与 Compilation(单次编译)展开,面试常考 Tapable 钩子机制与编译、优化、输出三阶段。
一句话概括
Webpack 的构建流程就是一次”递归扫描 + 流水线加工”:从 entry 出发遍历所有 import 建立依赖图,Loader 管道逐层转换每个模块,Plugin 通过 Tapable 钩子在编译、优化、输出的每个阶段注入自定义逻辑——整个流程围绕 Compiler(全局管家)和 Compilation(单次编译)两个对象展开。
核心知识点
1. 三阶段总览:初始化 → 编译 → 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
配置文件 + CLI 参数
│
▼
┌──────────────┐
│ ① 初始化 │ Compiler 创建、Plugin 注册(apply 被调用)
│ 约 100ms │
└──────┬───────┘
│ compiler.run()
▼
┌──────────────┐
│ ② 编译 │ Compilation 创建 → Entry 解析 → 递归构建模块依赖图
│ 占 80%+ │ → Loader 转换 → 依赖收集 → 重复直到无新模块
└──────┬───────┘
│
▼
┌──────────────┐
│ ③ 输出 │ 生成 Chunk → 优化(Tree Shaking/压缩)→ 写入 dist
│ 约 15-20% │
└──────────────┘
每个阶段都通过 Tapable 暴露钩子,Plugin 可以精确控制介入时机。
2. Compiler vs Compilation:两个最核心对象
1
2
3
4
5
6
7
// Compiler:一次 webpack 生命周期只有一个(全局单例)
const compiler = webpack(config);
// Compilation:每次构建都创建新的(watch 模式下每次文件变更都重建)
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
// compilation 拥有本次构建的所有数据:modules、chunks、assets...
});
| Compiler | Compilation | |
|---|---|---|
| 生命周期 | 一次启动 → 关闭 | 一次构建开始 → 结束 |
| 持有数据 | 配置、插件、全局钩子 | 模块图、Chunk、Asset |
| 典型钩子 | run, watchRun, done, failed | buildModule, optimizeChunks, emit |
3. 模块解析的递归过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 伪代码:make 阶段核心逻辑
function buildModule(module, compilation) {
// 1. 读取源文件
let source = fs.readFileSync(module.path, 'utf-8');
// 2. Loader 链转换(从右到左执行 normal,从左到右执行 pitch)
source = compilation.applyLoaders(module, source);
// 3. 解析 AST,提取 import/require 语句
const dependencies = parseAST(source); // acorn 做解析
// 4. 递归处理依赖
for (const dep of dependencies) {
const resolved = resolveModule(dep, module.context); // 模块解析算法
const childModule = createModule(resolved);
compilation.addModule(childModule);
buildModule(childModule, compilation); // 递归
}
// 5. 标记模块完成
compilation.finishModule(module);
}
模块解析算法:import './utils' 会按顺序查找 ./utils.js → ./utils.json → ./utils/index.js → ./utils/package.json(配 resolve.extensions 和 resolve.mainFields)
4. Chunk 生成策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
optimization: {
splitChunks: {
chunks: 'all', // 对所有 chunk 生效(包括异步)
cacheGroups: {
vendor: {
test: /node_modules/, // node_modules 的代码抽到 vendor chunk
name: 'vendor',
priority: 10, // 优先级高于 default
},
common: {
minChunks: 2, // 被 ≥2 个 chunk 引用才抽取
reuseExistingChunk: true,
}
}
},
runtimeChunk: 'single', // 把 webpack 运行时代码单独抽出(利于长期缓存)
}
Chunk ≠ 入口文件。一个 entry 可以产出多个 chunk(通过动态 import),多个 entry 的共享代码也可以合并到一个 chunk(splitChunks)。Chunk 是输出文件的逻辑分组。
5. Tapable:Webpack 的”插件总线”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Webpack 内部大量使用 Tapable 的 9 种钩子
const { SyncHook, AsyncParallelHook } = require('tapable');
compiler.hooks = {
make: new AsyncParallelHook(['compilation']), // 异步并行
compilation: new SyncHook(['compilation', 'params']), // 同步
emit: new AsyncSeriesHook(['compilation']), // 异步串行
done: new SyncHook(['stats']),
};
// Plugin 注册:tap(同步)/ tapAsync(异步回调)/ tapPromise(返回 Promise)
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 在文件输出到磁盘前修改 asset
const assets = compilation.getAssets();
// ...
callback();
});
Tapable 是 Webpack 插件系统的核心——没有它,所有 Plugin 就无法注入生命周期逻辑。
其实你每天都在用
npm run dev第一次启动慢、后续热更新快——第一次是完整构建(初始化 + 编译 + 输出),热更新只重建变化的模块及其依赖链,Compiler 实例复用、Compilation 对象重建。- 改一个文件,浏览器自动刷新——Webpack Dev Server 用
webpack-dev-middleware在内存中完成构建(不写磁盘),通过 WebSocket 推送 hash,浏览器对比后 hot accept 或 full reload。这一套流程依赖compiler.watch()+ HMR runtime。 - 生产构建比开发慢很多——因为生产模式下 MiniCssExtract、TerserPlugin、CssMinimizer、Tree Shaking 等优化阶段都在发挥作用。
optimization.minimize: true触发的压缩可能占构建时间的 30-50%。 - 一个 import 写错了路径,报错信息能精确告诉你查了哪些目录——Webpack 的
enhanced-resolve库在解析失败时会记录所有尝试过的路径,产生的错误信息就是你的调试线索。 thread-loader把 Babel 转换放到 worker 线程——Loader 默认在主线程串行执行,thread-loader利用了 Tapable 的异步钩子把耗时的转换分配到 Worker Pool,对于有大量.tsx文件的项目可提速 2-3 倍。
常见误解(FAQ)
❌ 误区:「Loader 是从左到右执行的」 真相:normal 阶段从右到左(
use: ['a', 'b', 'c']实际顺序是 c → b → a),但 pitch 阶段从左到右。如果某个 loader 的 pitch 返回了结果,后续 loader 的 normal 被跳过——这是 loader 链的”熔断”机制。❌ 误区:「
cache: true能解决所有性能问题」 真相:Webpack 5 的持久化缓存(filesystem cache)能跳过已缓存模块的重新构建,但模块解析(resolve)和 chunk 生成仍要执行。缓存最有效的场景是”只改了少量文件”的增量构建,全量构建缓存反而有读写开销。❌ 误区:「Compiler 和 Compilation 的钩子只是名字不同,功能差不多」 真相:Compiler 钩子用于”全局行为”(启动、停止、watch),Compilation 钩子用于”本次构建”(模块解析、优化、输出)。如果在 Compilation 钩子里只监听一次失效(因为每次构建重建 Compilation),必须用 Compiler 的
compilation钩子来注册。❌ 误区:「Tree Shaking 发生在 loader 转换阶段」 真相:Tree Shaking 发生在编译阶段结束后、输出阶段开始前的优化阶段。具体步骤:① 标记未使用的 exports(
usedExports),② 在 TerserPlugin 压缩时删除死代码。Loader 阶段只是把源码变成标准 ES Module,不负责判断哪些代码有用。
一句话总结
Webpack 的构建流程 = 从 entry 出发的一次图遍历 + 在每个节点上跑 loader 管道 + 在关键节点上触发 plugin 钩子——理解这个模型,性能优化、自定义 loader/plugin、调试构建报错三件事就都在你的射程内了。