FORMA

工作原理

Webpack 是静态模块打包器:从入口递归解析依赖,经 Loader 转换后输出一个或多个 bundle。与 Vite 对比见 Webpack 与 Vite

下面从构建流程、Tapable、Module/Chunk、依赖图与 Runtime 五方面说明。

一、完整构建流程

Webpack 的构建过程可以概括为以下阶段:

text
初始化参数 → 开始编译(创建 Compiler) → 确定入口 → 编译模块(递归查找依赖) → 构建模块(使用 Loader 转换) → 生成最终代码(Template + Chunk 优化) → 输出文件
  1. 初始化参数:读取 shell 命令或 webpack.config.js 配置,合并得到最终参数。
  2. 创建 Compiler:根据参数创建 Compiler 对象(核心调度器),并挂载所有内置插件和配置中的插件。
  3. 确定入口:从 entry 配置出发,解析入口文件的绝对路径。
  4. 编译模块make 阶段):
    • 从入口开始,通过 acorn 等解析器将文件内容转换为 AST(抽象语法树)。
    • 遍历 AST,收集 importrequiredefine 等依赖语句,记录依赖路径。
    • 递归此过程,得到完整的模块依赖图(Dependency Graph)。
  5. 构建模块:对每个模块,根据其文件类型匹配配置中的 rules,使用对应的 Loader 将源代码转换为 Webpack 可识别的 JavaScript 模块(例如将 SCSS 转 CSS,然后转成 JS 模块)。
  6. 生成最终代码
    • 将所有模块组合成 Chunk(根据入口和动态导入分割)。
    • 应用优化(如 splitChunksminimizersideEffects 过滤等)。
    • 通过 Template 类生成最终 bundle 的代码串(包含 __webpack_require__ 等运行时)。
  7. 输出文件:将生成的代码串写入到输出目录(output.path)。

整个流程由 CompilerCompilation 两个核心对象驱动,它们通过 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 / runcompiler.run() 开始前/后清理输出目录插件
compile创建新 Compilation 对象之前准备钩子
compilation创建 Compilation 对象后(参数为 compilation注册插件到 compilation 的钩子
make开始构建依赖图(递归编译模块)核心构建钩子,如 SingleEntryPlugin
buildModule单个模块开始构建前自定义 Loader 行为
succeedModule模块构建成功统计模块信息
finishModules所有模块构建完成分析依赖关系
seal停止接收新模块,开始优化和生成 Chunk生成 Chunk 逻辑
optimizeChunksChunk 优化阶段(splitChunks 在此生效)分割代码块
emit生成资源到输出目录前(可修改资源内容)内联资源、上传 CDN
afterEmit输出完成后清理临时文件
done整个打包完成构建完成通知

3. 手写插件时如何选择钩子

  • 需要修改模块内容:使用 compilation.hooks.buildModulenormalModuleLoader
  • 需要添加额外资源:使用 compilation.hooks.additionalAssetsemit
  • 需要改变依赖解析:使用 compilation.hooks.resolvenormalModuleFactory 钩子。
  • 需要影响打包结果:在 seal 之后到 emit 之前的钩子(如 optimizeChunksoptimizeModules)。
  • 写一个简单日志插件donecompile 即可。

注意:钩子有同步/异步之分,注册时需使用对应的 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

  1. 初始阶段:每个入口模块及其同步依赖形成一个初始 Entry Chunk
  2. 动态导入import() 会创建一个新的 Async Chunk
  3. 优化阶段(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,识别所有 importexportrequiredefine 等语句。
  • 对于每个依赖,记录其请求路径(request),并通过 resolve 机制将其转换为绝对路径。
  • 创建一个 Dependency 对象,并添加到当前模块的 dependencies 列表中。
  • 递归处理每个新找到的模块,直到所有模块都被访问。

2. Tree Shaking 的标记机制

  • usedExports:Webpack 在打包时,通过 optimization.usedExports 开启后,会利用 terser 等优化工具分析代码中哪些导出被使用了。
    • 对于 ESM 的 export,Webpack 会在模块构建时记录导出名称。
    • 在生成最终代码时,只保留被引用的导出,未使用的导出会被标记为 /* unused */,最后由压缩工具删除。
  • 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__.ohasOwnProperty 简写。
  • __webpack_require__.r:标记模块为 ES 模块。
  • __webpack_require__.n:兼容 CommonJS 模块的 default 导出。

这些函数被包裹在立即执行函数(IIFE)中,构成最终的 bundle。

总结

概念作用
构建流程配置初始化 → 创建 Compiler → 递归构建模块 → 生成 Chunk → 输出
Tapable提供插件钩子系统,控制整个构建生命周期的扩展点
Module/ChunkModule 是源文件单元,Chunk 是输出单元;splitChunks 利用引用计数拆分
依赖图谱AST 解析建立依赖关系;Tree Shaking 依赖 usedExports + sideEffects 标记删除
Runtime__webpack_require__ + manifest 提供浏览器端模块加载能力

理解这些原理,可以帮助我们更好地配置 Webpack、编写自定义插件,以及诊断打包问题。

参考文献

以下链接在编写时均可正常访问:

资料说明
Webpack 概念官方
Tapable钩子库
编译器钩子插件扩展

Series

webpack

6 / 6

Plugin