2025年10月15日7 分钟

Webpack 5 构建主流程线:从 Entry 到 Output 的完整链路

从源码层面追踪 Webpack 5 构建的完整流程线,理解初始化、编译、封装、输出四大阶段的每一步

构建流程全景

webpack(config)
     │
     ▼
┌─────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  初始化阶段   │ ──▶ │   编译阶段     │ ──▶ │   封装阶段     │ ──▶ │   输出阶段     │
│ Initialize  │     │   Make       │     │   Seal       │     │   Emit       │
│             │     │              │     │              │     │              │
│ 创建Compiler│     │ 模块构建      │     │ Module→Chunk │     │ 写入文件系统   │
│ 注册插件     │     │ 依赖解析      │     │ 代码优化      │     │ 生成产物      │
│ 合并配置     │     │ Loader转换   │     │ Tree Shaking │     │              │
└─────────────┘     └──────────────┘     └──────────────┘     └──────────────┘

第一阶段:初始化(Initialize)

// 入口:用户调用 webpack(config)

function webpack(config) {
  // 1. 合并配置(用户配置 + 默认配置)
  const mergedConfig = mergeWithDefaults(config)

  // 2. 创建 Compiler 实例
  const compiler = new Compiler(mergedConfig.context)
  compiler.options = mergedConfig

  // 3. 注册 Node 环境插件
  new NodeEnvironmentPlugin().apply(compiler)

  // 4. 注册用户配置的所有 Plugin
  if (mergedConfig.plugins) {
    for (const plugin of mergedConfig.plugins) {
      plugin.apply(compiler)  // 每个插件在这里注册钩子
    }
  }

  // 5. 根据 config 注册内置插件
  //    EntryPlugin — 处理入口
  //    ExternalsPlugin — 处理外部模块
  //    SplitChunksPlugin — 代码分割
  //    ...共 30+ 个内置插件
  compiler.hooks.environment.call()
  new WebpackOptionsApply().process(mergedConfig, compiler)

  return compiler
}

// 6. 调用 compiler.run() 开始构建
compiler.run((err, stats) => {
  console.log(stats.toString())
})
初始化阶段的钩子触发顺序:
  environment → afterEnvironment → entryOption →
  afterPlugins → afterResolvers → beforeRun → run

第二阶段:编译(Make)

这是最核心的阶段——从入口文件出发,递归构建所有模块

compiler.hooks.make 触发
        │
        ▼
  ┌─────────────────────────────────────────────┐
  │           EntryPlugin 开始处理入口              │
  │  compilation.addEntry('./src/index.tsx')     │
  └──────────────────┬──────────────────────────┘
                     │
                     ▼
            ┌────────────────┐
            │  创建 Module    │
            │  (ModuleFactory)│
            └────────┬───────┘
                     │
        ┌────────────▼────────────┐
        │    构建 Module (build)   │
        │                         │
        │  1. 执行 Loader 链       │
        │     tsx → swc-loader    │
        │     → JavaScript 源码   │
        │                         │
        │  2. Parser 解析 AST     │
        │     提取 import/require │
        │     → 依赖列表           │
        └────────────┬────────────┘
                     │
                     ▼
        ┌────────────────────────┐
        │  处理依赖                │
        │  (processModuleDeps)   │
        │                        │
        │  for each dependency:  │
        │    resolve → 路径解析    │
        │    create → 创建 Module │
        │    build → 递归构建      │  ← 回到上面
        └────────────────────────┘

详细的模块构建过程

// compilation.addEntry 触发后的流程

// Step 1: 创建入口模块
compilation._addEntryItem(entry, (err, module) => {
  // Step 2: 工厂创建模块
  normalModuleFactory.create({
    context: '/src',
    dependency: entryDependency,
  }, (err, module) => {

    // Step 3: resolve 路径
    // './src/index.tsx' → '/Users/project/src/index.tsx'
    // 同时确定需要的 Loader 列表

    // Step 4: 构建模块
    compilation.buildModule(module, (err) => {

      // 4a. runLoaders — 执行 Loader 链
      runLoaders({
        resource: '/Users/project/src/index.tsx',
        loaders: [
          { loader: 'swc-loader', options: { /* ... */ } },
        ],
      }, (err, result) => {
        module._source = result.source  // JavaScript 源码
      })

      // 4b. parse — 解析 AST,提取依赖
      parser.parse(module._source)
      // → module.dependencies = [
      //     HarmonyImportDependency('react'),
      //     HarmonyImportDependency('./App'),
      //     HarmonyImportDependency('./styles.css'),
      //   ]

      // Step 5: 递归处理所有依赖
      compilation.processModuleDependencies(module, (err) => {
        // 对每个依赖重复 Step 2-5
        // 直到所有模块都构建完成
      })
    })
  })
})

模块依赖图的形成

index.tsx
  ├── import React from 'react'
  │     └── react/index.js
  │           └── react/cjs/react.production.min.js
  ├── import App from './App'
  │     └── App.tsx
  │           ├── import Header from './Header'
  │           │     └── Header.tsx
  │           └── import './App.css'
  │                 └── App.css
  └── import './global.css'
        └── global.css

最终 ModuleGraph 包含以上所有模块及其依赖关系

第三阶段:封装(Seal)

所有模块构建完成后,进入封装阶段——把 Module 组织成 Chunk

compilation.seal()
        │
        ▼
  ┌─────────────────────────┐
  │ 1. 创建 EntryPoint Chunk │
  │    每个 entry → 一个 Chunk │
  │    main → ChunkA          │
  └───────────┬───────────────┘
              │
  ┌───────────▼───────────────┐
  │ 2. 处理异步依赖             │
  │    import() → 新的 Chunk    │
  │    lazy('./Page') → ChunkB │
  └───────────┬───────────────┘
              │
  ┌───────────▼───────────────┐
  │ 3. SplitChunksPlugin       │
  │    提取公共模块 → ChunkC    │
  │    vendors → ChunkD        │
  └───────────┬───────────────┘
              │
  ┌───────────▼───────────────┐
  │ 4. 优化阶段                 │
  │    Tree Shaking            │
  │    Scope Hoisting          │
  │    Module Concatenation    │
  │    代码压缩 (Terser/SWC)   │
  └───────────┬───────────────┘
              │
  ┌───────────▼───────────────┐
  │ 5. 生成代码                 │
  │    Module → 最终 JS 代码    │
  │    添加 runtime 运行时代码   │
  │    生成 Hash               │
  └───────────────────────────┘

Chunk 的生成逻辑

// 简化的 Chunk 生成过程
function seal() {
  // 1. 为每个入口创建 Chunk
  for (const [name, entry] of this.entries) {
    const chunk = new Chunk(name)
    const entrypoint = new Entrypoint(name)
    entrypoint.pushChunk(chunk)

    // 将入口模块和它的同步依赖放入 Chunk
    this.chunkGraph.connectChunkAndModule(chunk, entry.module)
    for (const dep of entry.module.dependencies) {
      if (dep.isSync) {
        this.chunkGraph.connectChunkAndModule(chunk, dep.module)
      }
    }
  }

  // 2. 异步导入创建新 Chunk
  for (const module of this.modules) {
    for (const dep of module.dependencies) {
      if (dep instanceof ImportDependency) {
        const asyncChunk = new Chunk()
        // import('./Page') → 单独的 Chunk
        this.chunkGraph.connectChunkAndModule(asyncChunk, dep.module)
      }
    }
  }

  // 3. SplitChunksPlugin 优化
  this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)

  // 4. Tree Shaking
  this.hooks.optimizeTree.callAsync(this.chunks, this.modules)

  // 5. 代码生成
  this.createChunkAssets()
}

第四阶段:输出(Emit)

// compiler.emitAssets()
function emitAssets(compilation, callback) {
  // 1. 触发 emit 钩子(最后修改产物的机会)
  this.hooks.emit.callAsync(compilation, (err) => {

    // 2. 获取输出配置
    const outputPath = this.options.output.path  // /dist

    // 3. 写入每个文件
    for (const [filename, source] of Object.entries(compilation.assets)) {
      const targetPath = path.join(outputPath, filename)

      // 确保目录存在
      mkdirp.sync(path.dirname(targetPath))

      // 写入文件
      fs.writeFileSync(targetPath, source.source())

      // 输出日志
      // ✓ main.a1b2c3.js    145 kB
      // ✓ vendors.d4e5f6.js   89 kB
      // ✓ styles.g7h8i9.css   12 kB
    }

    // 4. 触发 afterEmit 钩子
    this.hooks.afterEmit.callAsync(compilation, callback)
  })
}

最终输出的文件结构

dist/
├── main.a1b2c3d4.js         # 入口 Chunk
├── vendors.e5f6g7h8.js      # 第三方库 Chunk
├── async-page.i9j0k1l2.js   # 异步加载的页面
├── styles.m3n4o5p6.css       # 提取的 CSS
├── main.a1b2c3d4.js.map      # Source Map
└── index.html                # HTML 模板(HtmlWebpackPlugin)

完整钩子触发顺序

初始化阶段:
  environment → afterEnvironment → entryOption →
  afterPlugins → afterResolvers

运行阶段:
  beforeRun → run →

编译阶段:
  beforeCompile → compile → thisCompilation → compilation →
  make → [模块递归构建] → finishMake →

封装阶段:
  seal → optimizeDependencies →
  afterOptimizeDependencies → beforeChunks →
  afterChunks → optimize → optimizeModules →
  optimizeChunks → optimizeTree → afterOptimizeTree →
  processAssets → afterProcessAssets →
  afterSeal →

完成阶段:
  afterCompile → shouldEmit → emit → afterEmit → done

总结

阶段输入输出核心动作
初始化webpack.config.jsCompiler 实例合并配置、注册插件
编译Entry 入口路径ModuleGraph递归构建所有模块
封装ModuleGraphChunks + AssetsModule→Chunk、优化、代码生成
输出Assets磁盘文件写入 dist/