01先理清几个核心概念
理解流程前,需要先明确 Webpack 世界里的几个基本“角色”:
- Module(模块):在 Webpack 中,一切皆模块,一个文件就是一个模块。
- Chunk(代码块):是打包过程中的中间产物。一个 Chunk 由多个 Module 组合而成,用于代码的合并与分割。
- Bundle(捆绑包):是打包最终输出的文件。
- Loader(加载器):是一个转换器,负责将非 JS 文件(如 CSS、图片)或新版 JS 语法,翻译成 Webpack 能理解和处理的模块。
- Plugin(插件):是扩展器,它能在 Webpack 构建流程的特定生命周期节点上“钩入”,执行更广泛的任务,如压缩代码、生成 HTML、提取 CSS 等。
02 打包流程的四个核心阶段
Webpack 的构建是一个串行的过程,每次运行都遵循固定的阶段顺序。
1. 初始化阶段 (Initialization)
这个阶段是“做准备工作”。Webpack 会读取并合并 webpack.config.js 中的配置和命令行参数,然后创建一个全局唯一的 Compiler 对象。这个 Compiler 就像是构建过程的“中枢神经”,负责调度一切。同时,Webpack 会遍历所有配置的插件,调用它们的 apply 方法,让插件在构建的生命周期上注册好监听事件。
1const webpack = (config) => {
2 const compiler = new Compiler(config);
3 const { plugins } = config;
4 for (const plugin of plugins) {
5 plugin.apply(compiler);
6 }
7 return compiler;/* */
8};const webpack = (config) => {
9 const compiler = new Compiler(config);
10 const { plugins } = config;
11 for (const plugin of plugins) {
12 plugin.apply(compiler);
13 }
14 return compiler;/* */
15};
2. 构建阶段 (Build)
这是最核心的递归解析阶段。Webpack 从配置的 entry(入口文件)开始
- 读取与解析:读取入口文件内容,将其解析成抽象语法树(AST),找出所有的 import / require 语句。
- 调用 Loader:如果遇到的是非 JS 文件(如 .css 或 .ts),Webpack 会调用配置中对应的 Loader 链,将其转换为标准的 JS 模块
- 递归依赖:对于解析出的每一个依赖模块,重复上述“读取-解析-转换”的过程,并继续查找其内部依赖,直到所有相关文件都被处理。
- 结果:这个过程构建出了一张模块依赖图(ModuleGraph),清晰地记录了所有模块及其相互之间的引用关系。
1buildModule(name, modulePath) {
2 let sourceCode = fs.readFileSync(modulePath, "utf-8");
3 const moduleId = "./" + path.posix.relative(baseDir, modulePath);
4 let module = {
5 id: moduleId,
6 names: [name],
7 deps: [],
8 _source: "",
9 };
10 let loaders = [];
11 let { rules = [] } = this.options.module;
12 rules.forEach((rule) => {
13 let { test } = rule;
14 if (modulePath.match(test)) {
15 loaders.push(...rule.use);
16 }
17 });
18 sourceCode = loaders.reduceRight((code, loader) => {
19 return loader(code);
20 }, sourceCode);
21 console.log("sourceCode", sourceCode);
22 let ast = parser.parse(sourceCode, { sourceType: "module" });
23 traverse(ast, {
24 // 特定表达式
25 CallExpression: (nodePath) => {
26 const { node } = nodePath;
27 // console.log("CallExpression", node.callee.name);
28 if (node.callee.name === "require") {
29 let depModulePath = node.arguments[0].value; // 获取依赖模块
30 const dirname = path.posix.dirname(modulePath); // 获取当前编译模块的目录
31 console.log(dirname);
32 let depModuleRealPath = path.posix.join(dirname, depModulePath); //获取依赖模块的绝对路径
33 console.log(depModuleRealPath);
34 const ext = this.options.resolve?.extensions || [".js"]; // 文件后缀名
35 depModuleRealPath = tryExt(depModuleRealPath, ext);
36 // 放到全局依赖中,watch变化时用到
37 this.deps.push(depModuleRealPath);
38 const depModuleId =
39 "./" + path.posix.relative(baseDir, depModuleRealPath);
40 console.log("depModuleId", depModuleId);
41 // 将抽象语法树中的依赖模块改成依赖“模块id”, require("./deps2/d") => require("./src/deps2/d")
42 node.arguments = [types.stringLiteral(depModuleId)];
43 module.deps.push({ depModuleId, depModuleRealPath });
44 }
45 },
46 // import声明
47 ImportDeclaration: (nodePath) => {
48 const { node } = nodePath;
49 // console.log("ImportDeclaration", node.source.value);
50 let depModulePath = node.source.value; // 获取依赖模块
51 const dirname = path.posix.dirname(modulePath);
52 const depModuleRealPath = path.posix.join(dirname, depModulePath);
53 },
54 });
55 // 用新的ast树生成新的代码
56 let { code } = generator(ast);
57 module._source = code;
58 console.log("module", module);
59 // 递归解析依赖
60 module.deps.forEach(({ depModuleId, depModuleRealPath }) => {
61 let existModule = this.modules.find((item) => item.id === depModuleId);
62 if (existModule) {
63 //names指的是它属于哪个代码块chunk
64 existModule.names.push(name);
65 } else {
66 let depModule = this.buildModule(name, depModuleRealPath);
67 this.modules.push(depModule);
68 }
69 });
70
71 return module;
72}
73
74build(cb) {
75 let entry = {};
76 if (typeof this.options.entry === "string") {
77 entry.main = this.options.entry;
78 } else {
79 entry = this.options.entry;
80 }
81 console.log("entry", entry);
82 for (const entryName in entry) {
83 let entryPath = path.posix.join(baseDir, entry[entryName]);
84 console.log("entryPath", entryPath);
85 this.deps.push(entryPath);
86 let entryModule = this.buildModule(entryName, entryPath);
87 this.modules.push(entryModule);
88 console.log("this.modules", this.modules);
89 let chunk = {
90 name: entryName,
91 entryModule,
92 modules: this.modules.filter((item) => item.names.includes(entryName)), // 找出属于该代码块的模块
93 };
94 this.chunks.push(chunk);
95 }
96
97 this.chunks.forEach((chunk) => {
98 let filename = this.options.output.filename.replace("[name]", chunk.name);
99 this.assets[filename] = getSource(chunk);
100 });
101
102 cb(
103 null,
104 {
105 chunks: this.chunks,
106 modules: this.modules,
107 assets: this.assets,
108 },
109 this.deps
110 );
111 }
3. 优化与封装阶段 (Seal & Optimization)
当依赖图构建完成后,Webpack 会“封装”编译,进入优化环节。
- 生成 Chunk:根据入口和模块间的依赖关系,将模块组合成一个个 Chunk。默认情况下,每个入口会生成一个主 Chunk(Initial Chunk),而通过动态 import() 引入的模块或使用 SplitChunksPlugin 拆分的代码,会生成额外的 非初始 Chunk,用于实现代码分割和懒加载。
- 优化:在此阶段,Webpack 会执行一系列优化,如 Tree Shaking(移除未使用的死代码)和 Scope Hoisting(将模块合并到一个函数作用域中以减少代码体积)。
4. 输出阶段 (Emit)
Webpack 会根据配置的 output 规则(如 filename 和 path),将每个 Chunk 转换成最终的资源文件(Bundle),并写入到文件系统的指定目录中(通常是 dist)。如果开启了 devtool,此阶段还会生成 Source Map 文件,以便于调试。