通过Minipack 学习 webpack 构建原理

61 阅读2分钟

太长不看

// 1. 创建资源 (createAsset)
function createAsset(filename) {
  // 1.1 读取文件内容
  const content = readFile(filename)
  
  // 1.2 解析 AST
  const ast = parseAST(content)
  
  // 1.3 收集依赖
  const dependencies = collectDependencies(ast)
  
  // 1.4 转换代码
  const code = transformCode(ast)
  
  return {
    id: generateId(),
    filename,
    dependencies,
    code
  }
}

// 2. 创建依赖图 (createGraph)
function createGraph(entry) {
  // 2.1 从入口文件开始
  const mainAsset = createAsset(entry)
  const queue = [mainAsset]
  
  // 2.2 遍历所有依赖
  for (asset of queue) {
    asset.mapping = {}
    
    // 2.3 处理每个依赖
    for (dependency of asset.dependencies) {
      const child = createAsset(dependency)
      asset.mapping[dependency] = child.id
      queue.push(child)
    }
  }
  
  return queue
}

// 3. 打包 (bundle)
function bundle(graph) {
  // 3.1 生成模块代码
  const modules = graph.map(mod => {
    return `${mod.id}: [
      function(require, module, exports) { ${mod.code} },
      ${mod.mapping}
    ]`
  })
  
  // 3.2 生成最终代码
  return `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id]
        
        function localRequire(name) {
          return require(mapping[name])
        }
        
        const module = { exports: {} }
        fn(localRequire, module, module.exports)
        return module.exports
      }
      
      require(0)
    })({${modules}})
  `
}

核心概念

  • ast 抽象语法树
  • 树的遍历算法
  • 构建依赖关系图
  • IIFE

源码

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');

let ID = 0;

function createAsset(filename) {
  console.log(`\n[创建资源] 开始处理文件: ${filename}`);
  
  const content = fs.readFileSync(filename, 'utf-8');
  console.log(`[创建资源] 文件内容长度: ${content.length} 字符`);

  const ast = babylon.parse(content, {
    sourceType: 'module',
  });
  console.log(`[创建资源] AST解析完成,节点数量: ${ast.program.body.length}`);

  const dependencies = [];

  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
      console.log(`[创建资源] 发现依赖: ${node.source.value}`);
    },
  });

  const id = ID++;
  console.log(`[创建资源] 分配模块ID: ${id}`);
  console.log(`[创建资源] 依赖列表:`, dependencies);

  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });
  console.log(`[创建资源] 代码转换完成,转换后代码长度: ${code.length}`);
  console.log(`[创建资源] 转换后代码预览:\n${code.slice(0, 200)}...`);

  return {
    id,
    filename,
    dependencies,
    code,
  };
}

function createGraph(entry) {
  console.log(`\n[创建依赖图] 开始处理入口文件: ${entry}`);
  
  const mainAsset = createAsset(entry);
  console.log(`[创建依赖图] 入口文件处理完成,ID: ${mainAsset.id}`);
  console.log(`[创建依赖图] 入口文件依赖:`, mainAsset.dependencies);

  const queue = [mainAsset];
  console.log(`[创建依赖图] 初始化队列,当前长度: ${queue.length}`);

  for (const asset of queue) {
    console.log(`\n[创建依赖图] 处理模块: ${asset.filename} (ID: ${asset.id})`);
    asset.mapping = {};

    const dirname = path.dirname(asset.filename);
    console.log(`[创建依赖图] 模块目录: ${dirname}`);

    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);
      console.log(`[创建依赖图] 处理依赖: ${relativePath} -> ${absolutePath}`);

      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      console.log(`[创建依赖图] 添加依赖映射: ${relativePath} -> ${child.id}`);
      console.log(`[创建依赖图] 当前模块映射:`, asset.mapping);

      queue.push(child);
      console.log(`[创建依赖图] 队列长度更新为: ${queue.length}`);
    });
  }

  console.log(`\n[创建依赖图] 完成! 总共处理了 ${queue.length} 个模块`);
  console.log(`[创建依赖图] 最终依赖图:`, queue.map(asset => ({
    id: asset.id,
    filename: asset.filename,
    dependencies: asset.dependencies,
    mapping: asset.mapping
  })));
  return queue;
}

function bundle(graph) {
  console.log(`\n[打包] 开始打包 ${graph.length} 个模块`);
  
  let modules = '';

  graph.forEach(mod => {
    console.log(`[打包] 处理模块 ${mod.id}: ${mod.filename}`);
    console.log(`[打包] 模块依赖映射:`, mod.mapping);
    console.log(`[打包] 模块代码长度: ${mod.code.length}`);
    console.log(`[打包] 模块代码预览:\n${mod.code.slice(0, 200)}...`);
    
    modules += `${mod.id}: [
      function (require, module, exports) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  console.log(`[打包] 所有模块处理完成,开始生成最终代码`);
  console.log(`[打包] 模块字符串长度: ${modules.length}`);

  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `;

  console.log(`[打包] 打包完成! 最终代码长度: ${result.length} 字符`);
  return result;
}

const graph = createGraph('./example/entry.js');
const result = bundle(graph);

// 将打包结果写入文件
const outputPath = path.join(__dirname, 'bundle.js');
fs.writeFileSync(outputPath, result);
console.log(`\n[执行] 打包结果已写入文件: ${outputPath}`);

// 执行打包后的文件
console.log('\n[执行] 开始执行打包后的代码:');
require(outputPath);

日志

image.png

流程图

image.png

核心流程

核心流程说明

  1. 资源创建阶段
  • 读取源文件内容
  • 使用 babylon 解析成 AST
  • 收集所有 import 依赖
  • 使用 babel 转换代码为 CommonJS 格式
  1. 依赖图构建阶段
  • 从入口文件开始
  • 递归处理所有依赖
  • 为每个模块建立 ID 映射关系
  • 形成完整的依赖关系图
  1. 打包阶段
  • 将每个模块包装成函数
  • 实现 require 函数
  • 生成最终的 IIFE (立即执行函数)
  • 输出打包后的文件

项目

minipack