LOG // 054

webpack打包原理

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 方法,让插件在构建的生命周期上注册好监听事件。

ts
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),清晰地记录了所有模块及其相互之间的引用关系。
ts
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 文件,以便于调试。