工作原理概述
graph TD
初始化参数 --> 开始编译
开始编译 --> 确定入口
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成
单次执行流程如上图所示。在监听模式下,流程如下:
graph TD
初始化-->编译;
编译-->输出;
输出-->文件发生变化
文件发生变化-->编译
Webpack 打包流程详解
Webpack 构建流程可分为以下三大阶段:
- 初始化阶段:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译阶段:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 翻译文件内容,再递归处理该 Module 依赖的其他 Module
- 输出阶段:将编译后的 Module 组合成 Chunk,将 Chunk 转换为文件 assets,输出到文件系统
核心概念
-
Chunks:代码分割单元,包含:
- 模块依赖关系图
- 运行时逻辑
- 分组策略(通过 splitChunks 配置)
js{ id: "chunkId", files: ["main.js"], // 关联的assets _modules: [/* 模块列表 */], runtime: "runtime逻辑" }
-
Assets:输出资源,包含:
- 经过 Loader 处理的文件内容
- 压缩/优化后的代码
- 带 hash 的文件名
js{ "main.js": { source() { return "压缩后的代码" }, size() { return 文件大小 } } }
Plugin Hooks
可以通过 tap 方法为对应的 hook 注册回调函数,这些函数存储在 taps 属性中。
-
同步 Hooks(如 compilation):
-
使用 .tap() 注册的回调会在 hook 触发时立即执行
-
执行顺序遵循注册顺序
-
类型:
- SyncHook:顺序执行所有注册的回调函数,忽略返回值
- SyncBailHook:顺序执行所有注册的回调函数,当回调函数返回值不为undefined时,立即停止后续回调函数的执行,如 entryOption。这样当某个插件已经确定了最终配置时,可以跳过剩余插件的执行。
js// 多个插件处理 entry 配置 compiler.hooks.entryOption.tap('PluginA', () => { if (conditionA) return customEntry; // 条件满足时直接返回 }); compiler.hooks.entryOption.tap('PluginB', () => { // 如果 PluginA 已返回,这里不会执行 });
-
-
异步 Hooks(如 emit):
- AsyncSeriesHook:串行执行,插件回调按注册顺序依次执行。每个回调必须显式调用 callback() 或返回Promise。前一个回调完成后才会执行下一个。
- AsyncParallelHook:所有注册的回调同时触发,不保证执行顺序。需要每个回调显式调用 callback() 或返回Promise。
js
// taps 属性结构
compiler.hooks.environment.taps = [
{
name: 'MyPlugin', // 插件名称
type: 'sync', // 钩子类型
fn: Function, // 回调函数
stage: 0, // 执行优先级(仅异步 Hook 有效)
before: undefined // 指定执行顺序
}
]
// 指定执行顺序
compiler.hooks.environment.tap({
name: 'MyPlugin',
stage: 100, // 数字越大执行越晚
before: 'OtherPlugin' // 在指定插件前执行
}, () => { /*...*/ });
Compile 流程及对应 Hooks
1. 初始化阶段
js
const webpack = (options, callback) => {
// 一、初始化参数
// 1.1 参数校验
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
// 1.2 初始化 compiler
let compiler;
if (Array.isArray(options)) {
// 多配置处理
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
// 应用 Node 环境插件
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 1.3 加载用户自定义插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 触发环境钩子
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 注册内部插件
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
// 二、执行编译
if (callback) {
if (typeof callback !== "function") {
throw new Error("Invalid argument: callback");
}
// 监听模式
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
// 单次编译
compiler.run(callback);
}
return compiler;
};
事件 | Hook 类型 | 描述 |
---|---|---|
初始化参数 | - | 从配置文件和 Shell 语句中读取与合并参数,执行配置文件中的插件实例化语句 new Plugin() |
实例化 Compiler | - | 实例化 Compiler(负责文件监听和启动编译),包含完整的 webpack 配置,全局只有一个 Compiler 实例 |
加载插件 | - | 依次调用配置插件的 apply 方法,让插件可以监听后续的所有事件节点 |
environment afterEnvironment | SyncHook | 应用 Node.js 风格的文件系统到 compiler 对象,方便后续文件查找和读取 |
entryOption | SyncBailHook | 读取配置的 entry,可在此阶段修改配置 |
afterPlugins | SyncHook | 调用完所有内置和配置插件的 apply 方法后触发 |
afterResolvers | SyncHook | resolver 设置完成后触发(resolver 负责在文件系统中寻找指定路径的文件) |
initialize (v5) | SyncHook | 编译器对象初始化完毕触发 |
2. 编译阶段
编译时有两种模式:
- 监听模式(watch: true):监控文件系统变化,文件修改后触发完整重新编译
- 单次编译模式(默认):执行一次完整编译
事件 | Hook 类型 | 描述 |
---|---|---|
beforeRun | AsyncSeriesHook | 在开始执行一次构建之前调用(compiler.run 方法开始执行后立即调用) |
run | AsyncSeriesHook | 在开始读取 records(构建记录)之前调用,启动一次编译 |
watchRun | AsyncSeriesHook | 监听模式下,重新编译触发后(但在 compilation 实际开始前)执行 |
normalModuleFactory contextModuleFactory | SyncHook | 创建对应的工厂函数后执行。normalModuleFactory 负责创建普通模块(JS、CSS、图片等),contextModuleFactory 处理动态引入的模块(如 require.context) |
beforeCompile | AsyncSeriesHook | 创建 compilation parameter 后执行,可用于添加/修改 compilationParams |
compile | SyncHook | beforeCompile 之后立即调用(新的编译启动前) |
thisCompilation | SyncHook | 初始化 compilation 时调用(在触发 compilation 事件之前) |
compilation | SyncHook | 创建和管理模块构建过程的核心方法,主要功能: 1. 初始化 Compilation 实例(包含 modules/chunks/assets 等属性) 2. 提供构建流程的 hooks 系统 3. 协调核心操作: - 模块依赖图构建 - Loader 执行与源码转换 - 模块依赖解析 - 优化处理(如 tree shaking) - Chunks 生成 - 输出资源准备 注意:compilation hook 本身不直接执行构建,而是通过注册的插件和内部方法驱动上述流程 |
make | AsyncParallelHook | compilation 对象创建完毕,compilation 结束前执行(实际构建过程在此 hook 内完成) |
afterCompile | AsyncSeriesHook | compilation 结束后执行 |
3. 输出阶段
事件 | Hook 类型 | 描述 |
---|---|---|
shouldEmit | SyncBailHook | 在输出 asset 之前调用。返回一个布尔值,告知是否输出。如果返回 false ,则不会生成任何输出文件到 output.path 目录,但compilation.assets 对象仍会保留所有资源,只是跳过了写入磁盘的步骤。如 webpack-dev-server |
emit | AsyncSeriesHook | 输出 asset 到 output 目录前执行,可修改输出内容 |
assetEmitted | AsyncSeriesHook | 每个 asset 写入磁盘后立即触发,可访问输出路径和字节内容等信息 |
afterEmit | AsyncSeriesHook | 输出 asset 到 output 目录后执行,可生成额外分析报告 |
done | AsyncSeriesHook | 编译完成 |
Compilation 流程及对应 Hooks
模块构建执行时机
模块构建执行时机是在 make 阶段。webpack 在初始化阶段通过 new EntryOptionPlugin().apply(compiler)
加载 EntryOptionPlugin 插件(执行时机在 entryOption 阶段)。该插件遍历 entry 对象,为每个入口加载 SingleEntryPlugin 或 MultiEntryPlugin:
js
const itemToPlugin = (context, item, name) => {
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name);
}
return new SingleEntryPlugin(context, item, name);
};
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
if (typeof entry === "string" || Array.isArray(entry)) {
itemToPlugin(context, entry, "main").apply(compiler);
} else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
itemToPlugin(context, entry[name], name).apply(compiler);
}
} else if (typeof entry === "function") {
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
};
以 SingleEntryPlugin 为例:
js
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
// ...省略部分代码
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
在 make 阶段(已完成 compilation 构建)注册回调函数,通过执行 compilation.addEntry
启动构建流程。addEntry
方法通过 _addModuleChain
添加模块链并递归处理子依赖形成依赖图,然后通过 buildModule
执行完整构建流程(包括 Loader 执行和依赖解析)。
Compilation 常见 Hooks
事件 | Hook 类型 | 描述 |
---|---|---|
buildModule | SyncHook | 模块构建开始前触发,可修改模块源代码 |
normalModuleLoader | SyncHook | 普通模块 Loader,加载模块图中所有模块的函数 |
seal | SyncHook | 编译封存阶段触发,禁止再修改/新增模块依赖关系 |
optimizeModules | SyncBailHook | 模块优化阶段开始时调用,可对模块进行优化 |
afterOptimizeModules | SyncHook | 模块优化完成后触发,用于分析优化结果或生成报告 |
optimizeChunks | SyncBailHook | Chunk 优化阶段触发,用于合并或拆分 chunk |
afterOptimizeChunks | SyncHook | Chunk 优化完成后触发,常用于最终资源清单生成 |
optimizeTree | AsyncSeriesHook | 依赖树优化前触发,用于自定义依赖分析逻辑 |
additionalAssets | AsyncSeriesHook | 添加额外资源文件(如 favicon) |
optimizeChunkAssets | AsyncSeriesHook | 资源优化阶段触发,可用于代码压缩 |
afterOptimizeAssets | SyncHook | 资源优化完成后触发,用于计算最终文件哈希 |
流程总结
- 参数初始化阶段:从配置文件(默认webpack.config.js)读取与合并参数,得出最终的参数
- 实例化 Compiler:用参数初始化 Compiler 对象,加载所有配置的插件
- 编译启动阶段:根据 entry 找出所有入口文件,实例化对应的 Compilation 对象(创建和管理模块构建过程的核心方法)
- 模块编译阶段 (make 阶段):从入口文件出发,调用 Loader 翻译模块,并递归处理所有依赖模块。整个过程
compilation
各生命周期钩子被调用。 - Chunk 生成阶段:根据模块依赖关系将模块分组为 chunks(资源优化的关键步骤)
- Asset 生成阶段:通过模板引擎将 chunks 转换为最终 assets(存储在 compilation.assets 中)
- 文件输出阶段:根据配置确定输出路径和文件名,将内容写入文件系统