Webpack 构建流程

Webpack 的初始化、编译、输出三个阶段

问题

描述 Webpack 的构建流程,包括初始化、编译、输出三个阶段分别做了什么。

解答

Webpack 构建流程分为三个阶段:

1. 初始化阶段

读取配置,创建 Compiler 对象,加载插件。

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 入口配置
  entry: './src/index.js',
  
  // 输出配置
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash].js',
  },
  
  // 模块处理规则
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
    ],
  },
  
  // 插件
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};

初始化阶段的主要工作:

// 简化的初始化流程
class Compiler {
  constructor(options) {
    // 1. 合并配置(命令行参数 + 配置文件)
    this.options = this.mergeOptions(options);
    
    // 2. 初始化 hooks(基于 tapable)
    this.hooks = {
      run: new AsyncSeriesHook(['compiler']),
      compile: new SyncHook(['params']),
      emit: new AsyncSeriesHook(['compilation']),
      done: new AsyncSeriesHook(['stats']),
    };
    
    // 3. 加载插件
    if (options.plugins) {
      options.plugins.forEach(plugin => {
        plugin.apply(this);
      });
    }
  }
}

2. 编译阶段

从入口出发,递归解析依赖,构建模块依赖图。

// 简化的编译流程
class Compilation {
  constructor(compiler) {
    this.compiler = compiler;
    this.modules = [];      // 所有模块
    this.chunks = [];       // 代码块
    this.assets = {};       // 输出资源
  }

  // 构建模块
  buildModule(modulePath) {
    // 1. 读取源代码
    let sourceCode = fs.readFileSync(modulePath, 'utf-8');
    
    // 2. 调用 loader 转换
    sourceCode = this.runLoaders(modulePath, sourceCode);
    
    // 3. 解析 AST,收集依赖
    const ast = parser.parse(sourceCode, {
      sourceType: 'module',
    });
    
    const dependencies = [];
    
    // 4. 遍历 AST,找出 import/require
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },
      CallExpression: ({ node }) => {
        if (node.callee.name === 'require') {
          dependencies.push(node.arguments[0].value);
        }
      },
    });
    
    // 5. 转换代码(ES6 -> ES5)
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    });
    
    return {
      id: modulePath,
      code,
      dependencies,
    };
  }

  // 递归构建依赖图
  build(entry) {
    const entryModule = this.buildModule(entry);
    this.modules.push(entryModule);
    
    // 递归处理依赖
    for (const module of this.modules) {
      module.dependencies.forEach(dep => {
        const depPath = this.resolvePath(module.id, dep);
        if (!this.modules.find(m => m.id === depPath)) {
          const depModule = this.buildModule(depPath);
          this.modules.push(depModule);
        }
      });
    }
  }

  // 运行 loader
  runLoaders(modulePath, sourceCode) {
    const rules = this.compiler.options.module?.rules || [];
    
    for (const rule of rules) {
      if (rule.test.test(modulePath)) {
        // loader 从右到左执行
        const loaders = Array.isArray(rule.use) ? rule.use : [rule.use];
        
        for (let i = loaders.length - 1; i >= 0; i--) {
          const loader = require(loaders[i]);
          sourceCode = loader(sourceCode);
        }
      }
    }
    
    return sourceCode;
  }
}

3. 输出阶段

将模块组合成 chunk,生成最终文件。

class Compilation {
  // 生成 chunk
  seal() {
    // 根据入口创建 chunk
    const chunk = {
      name: 'main',
      modules: this.modules,
    };
    this.chunks.push(chunk);
  }

  // 生成最终代码
  createAssets() {
    for (const chunk of this.chunks) {
      const filename = this.compiler.options.output.filename
        .replace('[name]', chunk.name);
      
      // 生成自执行函数包裹的代码
      this.assets[filename] = this.generateBundle(chunk);
    }
  }

  // 生成 bundle 代码
  generateBundle(chunk) {
    const modules = {};
    
    chunk.modules.forEach(module => {
      modules[module.id] = module.code;
    });

    // 生成运行时代码
    return `
      (function(modules) {
        // 模块缓存
        const installedModules = {};
        
        // require 函数
        function __webpack_require__(moduleId) {
          // 检查缓存
          if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
          }
          
          // 创建模块并缓存
          const module = installedModules[moduleId] = {
            exports: {}
          };
          
          // 执行模块代码
          modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
          );
          
          return module.exports;
        }
        
        // 加载入口模块
        return __webpack_require__("${chunk.modules[0].id}");
      })(${JSON.stringify(modules)});
    `;
  }

  // 写入文件
  emitAssets() {
    const outputPath = this.compiler.options.output.path;
    
    // 确保目录存在
    if (!fs.existsSync(outputPath)) {
      fs.mkdirSync(outputPath, { recursive: true });
    }
    
    // 写入文件
    Object.entries(this.assets).forEach(([filename, content]) => {
      const filePath = path.join(outputPath, filename);
      fs.writeFileSync(filePath, content);
    });
  }
}

完整流程串联

class Compiler {
  run(callback) {
    // 触发 run 钩子
    this.hooks.run.callAsync(this, err => {
      if (err) return callback(err);
      
      // 开始编译
      this.compile((err, compilation) => {
        if (err) return callback(err);
        
        // 触发 emit 钩子
        this.hooks.emit.callAsync(compilation, err => {
          if (err) return callback(err);
          
          // 输出文件
          compilation.emitAssets();
          
          // 触发 done 钩子
          this.hooks.done.callAsync({ compilation }, callback);
        });
      });
    });
  }

  compile(callback) {
    // 触发 compile 钩子
    this.hooks.compile.call();
    
    // 创建 compilation
    const compilation = new Compilation(this);
    
    // 构建模块
    compilation.build(this.options.entry);
    
    // 生成 chunk
    compilation.seal();
    
    // 生成资源
    compilation.createAssets();
    
    callback(null, compilation);
  }
}

流程图

┌─────────────────────────────────────────────────────────────┐
│                        初始化阶段                            │
├─────────────────────────────────────────────────────────────┤
│  1. 读取配置文件 + 命令行参数                                 │
│  2. 创建 Compiler 对象                                       │
│  3. 初始化 hooks                                             │
│  4. 加载插件(调用 plugin.apply)                            │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                        编译阶段                              │
├─────────────────────────────────────────────────────────────┤
│  1. 从 entry 开始                                            │
│  2. 调用 loader 转换模块                                     │
│  3. 解析 AST,收集依赖                                       │
│  4. 递归处理依赖模块                                         │
│  5. 构建模块依赖图                                           │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                        输出阶段                              │
├─────────────────────────────────────────────────────────────┤
│  1. 根据依赖图生成 chunk                                     │
│  2. 生成运行时代码                                           │
│  3. 触发 emit 钩子(插件可修改输出)                         │
│  4. 写入文件到 output.path                                   │
└─────────────────────────────────────────────────────────────┘

关键点

  • 初始化:合并配置、创建 Compiler、加载插件、初始化 hooks
  • 编译:从入口递归解析,loader 转换代码,AST 分析依赖,构建依赖图
  • 输出:依赖图 → chunk → bundle 代码 → 写入文件
  • Tapable:整个流程通过 hooks 串联,插件可以在各阶段介入
  • Loader:负责文件转换,从右到左执行
  • Plugin:通过订阅 hooks 扩展功能,贯穿整个构建流程