工作原理
Webpack 是静态模块打包器:从入口递归解析依赖,经 Loader 转换后输出一个或多个 bundle。与 Vite 对比见 Webpack 与 Vite。
下面从构建流程、Tapable、Module/Chunk、依赖图与 Runtime 五方面说明。
一、完整构建流程
Webpack 的构建过程可以概括为以下阶段:
初始化参数 → 开始编译(创建 Compiler) → 确定入口 → 编译模块(递归查找依赖) → 构建模块(使用 Loader 转换) → 生成最终代码(Template + Chunk 优化) → 输出文件
- 初始化参数:读取 shell 命令或
webpack.config.js配置,合并得到最终参数。 - 创建 Compiler:根据参数创建
Compiler对象(核心调度器),并挂载所有内置插件和配置中的插件。 - 确定入口:从
entry配置出发,解析入口文件的绝对路径。 - 编译模块(
make阶段):- 从入口开始,通过
acorn等解析器将文件内容转换为 AST(抽象语法树)。 - 遍历 AST,收集
import、require、define等依赖语句,记录依赖路径。 - 递归此过程,得到完整的模块依赖图(
Dependency Graph)。
- 从入口开始,通过
- 构建模块:对每个模块,根据其文件类型匹配配置中的
rules,使用对应的 Loader 将源代码转换为 Webpack 可识别的 JavaScript 模块(例如将 SCSS 转 CSS,然后转成 JS 模块)。 - 生成最终代码:
- 将所有模块组合成 Chunk(根据入口和动态导入分割)。
- 应用优化(如
splitChunks、minimizer、sideEffects过滤等)。 - 通过
Template类生成最终 bundle 的代码串(包含__webpack_require__等运行时)。
- 输出文件:将生成的代码串写入到输出目录(
output.path)。
整个流程由 Compiler 和 Compilation 两个核心对象驱动,它们通过 Tapable 钩子机制在各个阶段暴露扩展点。
二、Tapable 事件流机制
Tapable 是 Webpack 内部实现的一个小型发布-订阅库,定义了多种钩子类型,插件可以通过 tap/tapAsync/tapPromise 注册回调,在特定时机被执行。
1. 核心钩子类型
| 类型 | 执行模式 | 典型应用场景 |
|---|---|---|
SyncHook | 同步顺序执行,不关心返回值 | 普通广播,如 compilation 结束 |
SyncBailHook | 同步执行,任一回调返回非 undefined 则停止 | 校验类,如某个 loader 返回结果 |
SyncWaterfallHook | 同步执行,上一个返回值作为下一个参数 | 链式转换,如 normalModuleLoader |
SyncLoopHook | 循环执行直到所有回调返回 undefined | 较少用 |
AsyncParallelHook | 异步并行执行(通过 tapAsync/tapPromise) | 多个独立任务,如多个 Loader 编译 |
AsyncSeriesHook | 异步串行执行(一个完成后执行下一个) | 递归构建模块,如 make 阶段 |
2. 关键钩子触发时机(Compiler / Compilation)
| 钩子名 | 触发时机 | 常用插件示例 |
|---|---|---|
environment / afterEnvironment | 读取配置完成后,创建 Compiler 前 | 设置环境变量 |
entryOption | 解析 entry 配置后 | 动态修改入口 |
beforeRun / run | compiler.run() 开始前/后 | 清理输出目录插件 |
compile | 创建新 Compilation 对象之前 | 准备钩子 |
compilation | 创建 Compilation 对象后(参数为 compilation) | 注册插件到 compilation 的钩子 |
make | 开始构建依赖图(递归编译模块) | 核心构建钩子,如 SingleEntryPlugin |
buildModule | 单个模块开始构建前 | 自定义 Loader 行为 |
succeedModule | 模块构建成功 | 统计模块信息 |
finishModules | 所有模块构建完成 | 分析依赖关系 |
seal | 停止接收新模块,开始优化和生成 Chunk | 生成 Chunk 逻辑 |
optimizeChunks | Chunk 优化阶段(splitChunks 在此生效) | 分割代码块 |
emit | 生成资源到输出目录前(可修改资源内容) | 内联资源、上传 CDN |
afterEmit | 输出完成后 | 清理临时文件 |
done | 整个打包完成 | 构建完成通知 |
3. 手写插件时如何选择钩子
- 需要修改模块内容:使用
compilation.hooks.buildModule或normalModuleLoader。 - 需要添加额外资源:使用
compilation.hooks.additionalAssets或emit。 - 需要改变依赖解析:使用
compilation.hooks.resolve或normalModuleFactory钩子。 - 需要影响打包结果:在
seal之后到emit之前的钩子(如optimizeChunks、optimizeModules)。 - 写一个简单日志插件:
done或compile即可。
注意:钩子有同步/异步之分,注册时需使用对应的 tap / tapAsync / tapPromise,且需要知道钩子预期接收的参数签名(参考 Webpack 文档)。
三、Module 与 Chunk 的关系
1. Module(模块)
- 代表文件级别的独立单元。
- 每个源文件(
.js、.css、.png、.vue等)经过 Loader 处理后,都会变成一个Module对象。 Module中存储了:文件路径、依赖列表(dependencies)、原始内容、转换后的代码、hash等信息。
2. Chunk(代码块)
- 一个或多个
Module的集合,最终输出为一个文件(bundle)。 - 分类:
- Entry Chunk:入口文件及其依赖形成的 chunk。
- Runtime Chunk:包含
__webpack_require__等引导代码的 chunk。 - Async Chunk:通过
import()动态导入产生的 chunk。 - Vendor Chunk:从
node_modules中抽取的第三方库。
3. Chunk Graph 的生成与 splitChunks
- 初始阶段:每个入口模块及其同步依赖形成一个初始
Entry Chunk。 - 动态导入:
import()会创建一个新的Async Chunk。 - 优化阶段(
optimization.splitChunks):- Webpack 会分析所有 Chunk 之间的模块重叠情况。
- 通过
SplitChunksPlugin提取公共模块(例如被多个入口/异步 chunk 引用的 lodash、vue 等)。 - 内部算法基于模块被引用次数、chunk 数量阈值、模块体积阈值等参数,决定是否提取为新 chunk。
- 提取后,原 chunk 中对公共模块的引用会被替换为对提取出的 chunk 的引用。
核心原理:SplitChunksPlugin 会生成一个新的 ChunkGroup,将公共模块移入,然后调整依赖关系。这是通过 compilation.hooks.optimizeChunks 钩子完成的。
四、依赖图谱(Dependency Graph)
1. 模块引用关系的建立
- 当 Webpack 读取一个模块文件时,会使用
acorn(或@babel/parser)将代码解析为 AST。 - 遍历 AST,识别所有
import、export、require、define等语句。 - 对于每个依赖,记录其请求路径(request),并通过
resolve机制将其转换为绝对路径。 - 创建一个
Dependency对象,并添加到当前模块的dependencies列表中。 - 递归处理每个新找到的模块,直到所有模块都被访问。
2. Tree Shaking 的标记机制
- usedExports:Webpack 在打包时,通过
optimization.usedExports开启后,会利用 terser 等优化工具分析代码中哪些导出被使用了。- 对于 ESM 的
export,Webpack 会在模块构建时记录导出名称。 - 在生成最终代码时,只保留被引用的导出,未使用的导出会被标记为
/* unused */,最后由压缩工具删除。
- 对于 ESM 的
- sideEffects:
- 通过在
package.json中声明"sideEffects": false,告知 Webpack 该包的所有模块都没有副作用(即仅导入但不使用时可安全删除)。 - 如果模块有副作用(如
import './polyfill'),则需标记"sideEffects": ["./polyfill.js"]。 - Webpack 在分析时,如果遇到
sideEffects: false的包,且仅引用了其中未使用的导出,则会完全跳过该模块的打包。
- 通过在
两者协作:sideEffects 控制模块级删除,usedExports 控制模块内部导出级删除。
五、Runtime 代码注入
Webpack 生成的 bundle 中除了模块代码外,还包含一套用于浏览器端加载、执行模块的运行时(Runtime)。核心包括 __webpack_require__ 函数和 manifest。
1. __webpack_require__ 模块加载函数
- 它维护一个
installedModules缓存对象(moduleId -> exports)。 - 接受
moduleId,如果缓存中存在则直接返回;否则创建一个新模块对象,执行模块函数,并缓存。 - 模块函数的代码中会调用
__webpack_require__来引入依赖。 - 支持动态导入:
__webpack_require__.e用于加载异步 chunk。
2. Manifest(清单)
- Manifest 是一个记录所有模块 ID 到其对应 chunk 文件路径(或 chunk ID)的映射表。
- 它帮助
__webpack_require__在运行时找到异步 chunk 的 URL。 - Manifest 可以单独提取为一个文件(通过
optimization.runtimeChunk: true),以便于缓存控制。
3. 其他辅助函数
__webpack_require__.d:定义 getter 使导出可被外部访问。__webpack_require__.o:hasOwnProperty简写。__webpack_require__.r:标记模块为 ES 模块。__webpack_require__.n:兼容 CommonJS 模块的 default 导出。
这些函数被包裹在立即执行函数(IIFE)中,构成最终的 bundle。
总结
| 概念 | 作用 |
|---|---|
| 构建流程 | 配置初始化 → 创建 Compiler → 递归构建模块 → 生成 Chunk → 输出 |
| Tapable | 提供插件钩子系统,控制整个构建生命周期的扩展点 |
| Module/Chunk | Module 是源文件单元,Chunk 是输出单元;splitChunks 利用引用计数拆分 |
| 依赖图谱 | AST 解析建立依赖关系;Tree Shaking 依赖 usedExports + sideEffects 标记删除 |
| Runtime | __webpack_require__ + manifest 提供浏览器端模块加载能力 |
理解这些原理,可以帮助我们更好地配置 Webpack、编写自定义插件,以及诊断打包问题。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Webpack 概念 | 官方 |
| Tapable | 钩子库 |
| 编译器钩子 | 插件扩展 |
相关文章
产物优化
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 等。基础见 工作原理。
Plugin
Webpack 插件在构建生命周期钩子上扩展能力(修改资源、生成 HTML、上传 CDN 等)。背景见 工作原理;与 Loader 分工:Loader 转换单文件,插件面向整体构建。
其他配置
除 ESLint、Prettier 等规范文件外,仓库中常见的工具链与包管理配置如下。见 工程化概览。
Series
webpack
6 / 6