理解 Webpack 的构建过程(实现原理),并实现一个 mini 版

理解 Webpack 的构建过程(实现原理),实现一个 super-mini 版

如果把 webpack 当成是一个函数的话,这个函数的功能就是将项目的各种文件转换成一个或多个浏览器可以直接使用的文件。转换的规则就是 webpack 的配置文件。所以函数的输入就是配置文件,函数的输出就是打包后的文件。函数的内部实现就是 webpack 的构建过程。 webpack 很像是一个搬家公司的打包服务,你把你的东西(各种文件)交给 webpack,webpack 就会把你的东西打包成一个或多个包裹(浏览器可以直接使用的文件)。

Webpack 的构建过程

完整示例项目github.com/frontzhm/mi...

Webpack 构建流程主要分为三大核心阶段,每个阶段都有其特定的职责和实现方式。

  • 初始化阶段:读取配置文件,创建 Compiler 实例,注册插件,解析 Entry 入口,创建依赖图,准备构建环境
  • 编译阶段:创建 Compilation 实例,解析模块,加载 Loader,转换文件,解析依赖,递归处理,生成 AST,优化代码,生成 Chunks
  • 输出阶段:生成资源,执行插件,优化资源,生成文件,写入磁盘,完成构建

用搬家公司的打包服务来比喻的话,就是:

  • 初始化阶段:搬家公司看到你的房子,确定打包的房间顺序,然后整理好各个房间的物品清单,根据物品清单准备打包材料
  • 编译阶段:搬家公司的工人根据物品清单来打包,不同的物品采用不同的包装材料,放进不同的打包盒子
  • 输出阶段:打包的盒子,进行后续的处理,物品进行压缩、封装、贴上标签等

代码实现:

js 复制代码
class MiniWebpack {
  constructor(config) {
    this.config = config; // 配置文件
  }
  /**
   * 执行构建流程
   */
  run() {
    console.log('🎯 开始 Mini Webpack 构建流程\n');
    // 阶段一:初始化
    this.initialize();
    // 阶段二:编译
    this.compile();
    // 阶段三:输出
    this.emit();
    console.log('🎉 构建成功完成!');
  }
  initialize() {}
  compile() {}
  emit() {}
}

使用的时候,创建一个 MiniWebpack 实例,然后调用 run 方法,就可以开始构建了。

js 复制代码
const MiniWebpack = require('./MiniWebpack');
const config = require('./webpack.config');

// 创建 MiniWebpack 实例并运行
const miniWebpack = new MiniWebpack(config);
miniWebpack.run();

好,构建过程说完了,mini 版的也说完了,不想看的,可以不用往下看了,哈哈哈。

说正事,下面开始每个阶段的核心实现。

🎯 阶段一:初始化阶段 (Initialization)

主要步骤:

  1. 读取配置 :解析 webpack.config.js 配置文件
  2. 创建 Compiler:实例化 Webpack 的编译器对象
  3. 注册插件 :执行所有插件的 apply 方法
  4. 解析入口:确定入口文件和依赖关系
  5. 创建依赖图:建立模块依赖关系图
graph TD A[启动 Webpack] --> B[读取配置文件] B --> C[创建 Compiler 实例] C --> D[注册插件] D --> E[解析 Entry 入口] E --> F[创建依赖图] F --> G[准备构建环境]

核心代码实现:

部分步骤比较复杂,先不实现,一言带过(主要我也不会),简单实现一下,主要是为了让大家有个大概的印象(就是写我会的部分,哈哈哈)。

javascript 复制代码
class MiniWebpack {
  // 阶段一:初始化阶段
  initialize() {
    console.log('🚀 阶段一:初始化阶段');
    console.log('📖 读取配置文件:', this.config); // 其实就是拿到this.config,实际的时候应该是需要和命令行参数结合的
    console.log('🔧 创建 Compiler 实例');
    console.log('📝 解析 Entry 入口:', this.config.entry); // 其实就是拿到this.config.entry
    console.log('✅ 初始化完成\n');
  }
}

🔄 阶段二:编译阶段 (Compilation)

主要步骤:

  1. 创建 Compilation:为每次构建创建编译实例
  2. 解析模块:从入口开始解析所有模块
  3. 加载 Loader:根据文件类型加载对应的 Loader
  4. 转换文件:将各种格式文件转换为 JavaScript
  5. 解析依赖:分析模块间的依赖关系
  6. 递归处理:深度遍历所有依赖模块
  7. 生成 AST:将代码转换为抽象语法树
  8. 优化代码:进行代码优化和压缩
  9. 生成 Chunks:将模块组合成代码块
graph TD A[开始编译] --> B[创建 Compilation] B --> C[解析模块] C --> D[加载 Loader] D --> E[转换文件] E --> F[解析依赖] F --> G[递归处理] G --> H[生成 AST] H --> I[优化代码] I --> J[生成 Chunks]

核心代码实现:

这里开始变得有点难度了。

先在 MiniWebpack 类中添加一些属性,用于存储模块、依赖图和模块 ID 计数器。 然后实现 compile 方法,其实就是将入口文件路径,传给parseModule函数,递归解析所有依赖,并存储到 modules 和 graph 中。

parseModule 方法的实现逻辑:

  • 检查模块是否已经解析过,避免重复解析,解析过的文件,则module[filePath]直接返回,避免重复解析
  • 没解析过的文件,生成新的模块对象,包括 id、filePath、source、dependencies,其中 source 是转换后的代码,dependencies 是依赖的文件路径数组如['./a.js', './b.js']
  • 存储到模块映射,module[filePath] = module
  • 添加到依赖图,graph.push(module)
  • 递归解析依赖 dependencies,forEach(dep => parseModule(dep))
  • 返回模块对象
javascript 复制代码
class MiniWebpack {
  constructor(config) {
    this.config = config; // 配置文件
    this.modules = new Map(); // 存储所有模块,key是文件路径,value是模块对象
    this.graph = []; // 依赖图,存储所有模块的依赖关系,数组中存储的是模块对象
  }
  // 阶段二:编译阶段
  async compile() {
    console.log('🔄 阶段二:编译阶段');

    // 从入口开始解析
    const entryPath = path.resolve(this.config.entry);
    console.log('📂 开始解析入口文件:', entryPath);

    // 递归解析所有依赖,parseModule方法会递归解析所有依赖,并存储到modules和graph中
    await this.parseModule(entryPath);

    console.log('✅ 编译阶段完成\n');
  }
  async parseModule(filePath) {
    // 确保filePath是绝对路径
    filePath = path.resolve(filePath);
    if (this.modules.has(filePath)) {
      return this.modules.get(filePath);
    }
    const newSource = ''; // 等会补充,先空着
    const dependencies = []; // 等会补充,先空着
    const module = {
      id: filePath,
      filePath: filePath,
      source: transformedCode,
      dependencies: dependencies.map((dep) =>
        this.resolveModulePath(filePath, dep)
      ),
    };

    this.modules.set(filePath, module);
    this.graph.push(module);

    // 递归解析依赖
    for (const dep of module.dependencies) {
      if (fs.existsSync(dep)) {
        await this.parseModule(dep);
      }
    }

    return module;
  }
  resolveModulePath(currentPath, modulePath) {
    // 等会补充,先空着
  }
}

接下来重点看下sourcedependencies的实现。

sourcedependencies的实现

sourcedependencies的实现逻辑:

  • 读取文件内容
  • 判断文件类型
  • 如果是 js 文件,使用 AST 解析和代码转换和代码生成,dependencies 是依赖的文件路径数组
  • 如果是非 js 文件,不存在 AST 解析和代码转换和代码生成,直接使用正则表达式提取依赖,dependencies是依赖的文件路径数组
  • resolveModulePath解析模块路径,其实就是根据当前文件路径和依赖文件路径,生成依赖文件的绝对路径
js 复制代码
class MiniWebpack {
  /**
   * 解析单个模块
   */
  async parseModule(filePath) {
    if (this.modules.has(filePath)) {
      return this.modules.get(filePath);
    }

    console.log(`  🔍 解析模块: ${filePath}`);

    // 读取文件内容
    const source = fs.readFileSync(filePath, 'utf-8');

    // 检查文件类型
    const ext = path.extname(filePath);
    let dependencies = [];
    let transformedCode = source;

    // 只对 JavaScript 文件进行 AST 解析
    const isJavaScriptFile = ['.js', '.jsx', '.ts', '.tsx'].includes(ext);
    if (!isJavaScriptFile) {
      dependencies = this.extractDependenciesSimple(source);
    } else {
      // 使用 Babel 解析 AST
      const ast = parse(source, {
        sourceType: 'module',
        plugins: [
          'jsx',
          'typescript',
          'decorators-legacy',
          'classProperties',
          'objectRestSpread',
        ],
      });

      // 提取依赖
      dependencies = this.extractDependenciesFromAST(ast);

      // transformFromAst包含代码转换和代码生成
      const { code } = transformFromAst(ast, source, {
        presets: ['@babel/preset-env'],
        plugins: [
          // 将 ES6 模块转换为 CommonJS
          [
            '@babel/plugin-transform-modules-commonjs',
            {
              strictMode: false,
            },
          ],
        ],
      });

      transformedCode = code;
    }

    const module = {
      id: filePath,
      filePath: filePath,
      source: transformedCode,
      dependencies: dependencies.map((dep) =>
        this.resolveModulePath(filePath, dep)
      ),
    };

    this.modules.set(filePath, module);
    this.graph.push(module);

    // 递归解析依赖
    for (const dep of module.dependencies) {
      if (fs.existsSync(dep)) {
        await this.parseModule(dep);
      }
    }

    return module;
  }

  /**
   * 从 AST 中提取依赖
   */
  extractDependenciesFromAST(ast) {
    const dependencies = [];

    traverse(ast, {
      // 处理 import 语句
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },

      // 处理 require 语句
      CallExpression: ({ node }) => {
        if (node.callee.name === 'require' && node.arguments.length > 0) {
          const arg = node.arguments[0];
          if (arg.type === 'StringLiteral') {
            dependencies.push(arg.value);
          }
        }
      },

      // 处理动态 import
      CallExpression: ({ node }) => {
        if (node.callee.type === 'Import' && node.arguments.length > 0) {
          const arg = node.arguments[0];
          if (arg.type === 'StringLiteral') {
            dependencies.push(arg.value);
          }
        }
      },
    });

    return dependencies;
  }

  /**
   * 简单的依赖提取(使用正则表达式)
   */
  extractDependenciesSimple(source) {
    const dependencies = [];

    // 匹配 import 语句
    const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
    let match;
    while ((match = importRegex.exec(source)) !== null) {
      dependencies.push(match[1]);
    }

    // 匹配 require 语句
    const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
    while ((match = requireRegex.exec(source)) !== null) {
      dependencies.push(match[1]);
    }

    return dependencies;
  }

  /**
   * 解析模块路径
   */
  resolveModulePath(currentPath, modulePath) {
    // 如果是相对路径
    if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
      const possiblePaths = [
        path.resolve(path.dirname(currentPath), modulePath),
        path.resolve(path.dirname(currentPath), modulePath + '.js'),
        path.resolve(path.dirname(currentPath), modulePath + '.ts'),
        path.resolve(path.dirname(currentPath), modulePath + '.jsx'),
        path.resolve(path.dirname(currentPath), modulePath + '.tsx'),
      ];

      for (const possiblePath of possiblePaths) {
        if (fs.existsSync(possiblePath)) {
          return possiblePath;
        }
      }
    }

    // 如果是绝对路径或 node_modules
    if (path.isAbsolute(modulePath) || !modulePath.startsWith('.')) {
      return modulePath;
    }

    return path.resolve(path.dirname(currentPath), modulePath);
  }
}

完整执行流程图

graph TD A[parseModule 开始] --> B{检查缓存} B -->|已存在| C[返回缓存模块] B -->|不存在| D[读取文件内容] D --> E{判断文件类型} E -->|JavaScript| F[AST 解析] E -->|其他文件| G[正则表达式提取] F --> H[提取依赖] G --> H H --> I[代码转换] I --> J[创建模块对象] J --> K[存储到缓存] K --> L[递归解析依赖] L --> M[返回模块对象]

📦 阶段三:输出阶段 (Emission)

主要步骤:

  1. 生成资源:根据 Chunks 生成最终资源
  2. 执行插件:运行所有插件的 emit 钩子
  3. 优化资源:进行最终的资源优化
  4. 生成文件:创建输出文件
  5. 写入磁盘:将文件写入到输出目录
  6. 完成构建:触发 done 钩子,构建完成
graph TD A[准备输出] --> B[生成资源] B --> C[执行插件] C --> D[优化资源] D --> E[生成文件] E --> F[写入磁盘] F --> G[完成构建]

这边生成最终的 bundle 代码,其实就是将模块代码和模块映射代码拼接在一起,然后写入到输出文件中。模块代码就是modules,模块映射代码就是moduleMap

核心代码实现:

javascript 复制代码
class MiniWebpack {
  /**
   * 阶段三:输出阶段
   */
  emit() {
    console.log('📦 阶段三:输出阶段');

    // 生成 bundle 代码
    const bundle = this.generateBundle();

    // 确保输出目录存在
    const outputDir = this.config.output.path;
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    // 写入文件
    const outputPath = path.join(outputDir, this.config.output.filename);
    fs.writeFileSync(outputPath, bundle);

    console.log('📁 输出文件:', outputPath);
    console.log('📏 文件大小:', (bundle.length / 1024).toFixed(2) + ' KB');
    console.log('✅ 构建完成!\n');
  }

  /**
   * 生成最终的 bundle 代码
   */
  generateBundle() {
    let modules = '';

    // 生成模块映射,直接使用文件路径作为 key
    this.modules.forEach((module) => {
      // 将模块源码中的相对路径替换为绝对路径
      let processedSource = module.source;

      // 替换 require 调用中的相对路径
      module.dependencies.forEach((dep) => {
        const relativePath = path.relative(path.dirname(module.filePath), dep);

        // 处理各种可能的 require 格式
        const patterns = [
          `require("${relativePath}")`,
          `require('${relativePath}')`,
          `require("./${relativePath}")`,
          `require('./${relativePath}')`,
          `require("../${relativePath}")`,
          `require('../${relativePath}')`,
          // 添加不带扩展名的版本
          `require("./${relativePath.replace(/\.js$/, '')}")`,
          `require('./${relativePath.replace(/\.js$/, '')}')`,
          `require("${relativePath.replace(/\.js$/, '')}")`,
          `require('${relativePath.replace(/\.js$/, '')}')`,
        ];

        patterns.forEach((pattern) => {
          if (processedSource.includes(pattern)) {
            processedSource = processedSource.replace(
              pattern,
              `require("${dep}")`
            );
          } else {
            // console.log(`    ❌ 未找到匹配: ${pattern}`);
          }
        });
      });

      modules += `"${module.id}": function(module, exports, require) {\n${processedSource}\n},\n`;
    });

    return `
(function(modules) {
  // 模块缓存
  var installedModules = {};
  
  // require 函数实现
  function __webpack_require__(moduleId) {
    // 检查缓存
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    
    // 创建新模块
    var module = installedModules[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {}
    };
    
    // 执行模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    // 标记为已加载
    module.loaded = true;
    
    // 返回模块导出
    return module.exports;
  }
  
  // 入口模块执行
  return __webpack_require__("${path.resolve(this.config.entry)}");
})({
${modules}
})`;
  }
}

MiniWebpack 完整代码

MiniWebpack 完整代码如下:

js 复制代码
const path = require('path');
const fs = require('fs');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
class MiniWebpack {
  constructor(config) {
    this.config = config; // 配置文件
    this.modules = new Map(); // 存储所有模块,key是文件路径,value是模块对象
    this.graph = []; // 依赖图,存储所有模块的依赖关系,数组中存储的是模块对象
  }
  /**
   * 执行构建流程
   */
  async run() {
    console.log('🎯 开始 Mini Webpack 构建流程\n');
    try {
      // 阶段一:初始化
      this.initialize();
      // 阶段二:编译
      await this.compile();
      // 阶段三:输出
      this.emit();
      console.log('🎉 构建成功完成!');
    } catch (error) {
      console.error('❌ 构建失败:', error.message);
      console.error(error.stack);
    }
  }
  // 阶段一:初始化阶段
  initialize() {
    console.log('🚀 阶段一:初始化阶段');
    console.log('📖 读取配置文件:', this.config); // 其实就是拿到this.config,实际的时候应该是需要和命令行参数结合的
    console.log('🔧 创建 Compiler 实例');
    console.log('📝 解析 Entry 入口:', this.config.entry); // 其实就是拿到this.config.entry
    console.log('✅ 初始化完成\n');
  }
  // 阶段二:编译阶段
  async compile() {
    console.log('🔄 阶段二:编译阶段');

    // 从入口开始解析
    const entryPath = path.resolve(this.config.entry);
    console.log('📂 开始解析入口文件:', entryPath);

    // 递归解析所有依赖
    await this.parseModule(entryPath);

    console.log('📊 依赖图构建完成:');
    this.graph.forEach((module, index) => {
      console.log(
        `  ${index + 1}. ${module.id} -> [${module.dependencies.join(', ')}]`
      );
    });

    console.log('✅ 编译阶段完成\n');
  }
  /**
   * 解析单个模块
   */
  async parseModule(filePath) {
    const absolutePath = path.resolve(filePath);

    if (this.modules.has(absolutePath)) {
      return this.modules.get(absolutePath);
    }

    console.log(`  🔍 解析模块: ${absolutePath}`);

    // 读取文件内容
    const source = fs.readFileSync(absolutePath, 'utf-8');

    // 检查文件类型
    const ext = path.extname(absolutePath);
    let dependencies = [];
    let transformedCode = source;

    // 只对 JavaScript 文件进行 AST 解析
    const isJavaScriptFile = ['.js', '.jsx', '.ts', '.tsx'].includes(ext);
    if (!isJavaScriptFile) {
      dependencies = this.extractDependenciesSimple(source);

      // 对于 CSS 文件,生成动态插入样式的代码
      if (ext === '.css') {
        transformedCode = `
// CSS 文件处理
const style = document.createElement('style');
style.textContent = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
      }
    } else {
      // 使用 Babel 解析 AST
      const ast = parse(source, {
        sourceType: 'module',
        plugins: [
          'jsx',
          'typescript',
          'decorators-legacy',
          'classProperties',
          'objectRestSpread',
        ],
      });

      // 提取依赖
      dependencies = this.extractDependenciesFromAST(ast);

      // 使用 Babel 转换代码
      const { code } = transformFromAst(ast, source, {
        presets: ['@babel/preset-env'],
        plugins: [
          // 将 ES6 模块转换为 CommonJS
          [
            '@babel/plugin-transform-modules-commonjs',
            {
              strictMode: false,
            },
          ],
        ],
      });

      transformedCode = code;
    }

    const module = {
      id: absolutePath, // 使用绝对路径作为模块 ID
      filePath: absolutePath, // 使用绝对路径
      source: transformedCode,
      dependencies: dependencies.map((dep) =>
        this.resolveModulePath(absolutePath, dep)
      ),
    };

    this.modules.set(absolutePath, module);
    this.graph.push(module);

    // 递归解析依赖
    for (const dep of module.dependencies) {
      if (fs.existsSync(dep)) {
        await this.parseModule(dep);
      }
    }

    return module;
  }

  /**
   * 从 AST 中提取依赖
   */
  extractDependenciesFromAST(ast) {
    const dependencies = [];

    traverse(ast, {
      // 处理 import 语句
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },

      // 处理 require 语句
      CallExpression: ({ node }) => {
        if (node.callee.name === 'require' && node.arguments.length > 0) {
          const arg = node.arguments[0];
          if (arg.type === 'StringLiteral') {
            dependencies.push(arg.value);
          }
        }
      },

      // 处理动态 import
      CallExpression: ({ node }) => {
        if (node.callee.type === 'Import' && node.arguments.length > 0) {
          const arg = node.arguments[0];
          if (arg.type === 'StringLiteral') {
            dependencies.push(arg.value);
          }
        }
      },
    });

    return dependencies;
  }

  /**
   * 简单的依赖提取(使用正则表达式)
   */
  extractDependenciesSimple(source) {
    const dependencies = [];

    // 匹配 import 语句
    const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
    let match;
    while ((match = importRegex.exec(source)) !== null) {
      dependencies.push(match[1]);
    }

    // 匹配 require 语句
    const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
    while ((match = requireRegex.exec(source)) !== null) {
      dependencies.push(match[1]);
    }

    return dependencies;
  }

  /**
   * 解析模块路径
   */
  resolveModulePath(currentPath, modulePath) {
    // 如果是相对路径
    if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
      const possiblePaths = [
        path.resolve(path.dirname(currentPath), modulePath),
        path.resolve(path.dirname(currentPath), modulePath + '.js'),
        path.resolve(path.dirname(currentPath), modulePath + '.ts'),
        path.resolve(path.dirname(currentPath), modulePath + '.jsx'),
        path.resolve(path.dirname(currentPath), modulePath + '.tsx'),
      ];

      for (const possiblePath of possiblePaths) {
        if (fs.existsSync(possiblePath)) {
          return possiblePath;
        }
      }
    }

    // 如果是绝对路径或 node_modules
    if (path.isAbsolute(modulePath) || !modulePath.startsWith('.')) {
      return modulePath;
    }

    return path.resolve(path.dirname(currentPath), modulePath);
  }

  /**
   * 阶段三:输出阶段
   */
  emit() {
    console.log('📦 阶段三:输出阶段');

    // 生成 bundle 代码
    const bundle = this.generateBundle();

    // 确保输出目录存在
    const outputDir = this.config.output.path;
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    // 写入文件
    const outputPath = path.join(outputDir, this.config.output.filename);
    fs.writeFileSync(outputPath, bundle);

    console.log('📁 输出文件:', outputPath);
    console.log('📏 文件大小:', (bundle.length / 1024).toFixed(2) + ' KB');
    console.log('✅ 构建完成!\n');
  }

  /**
   * 生成最终的 bundle 代码
   */
  generateBundle() {
    let modules = '';

    // 生成模块映射,直接使用文件路径作为 key
    this.modules.forEach((module) => {
      // 将模块源码中的相对路径替换为绝对路径
      let processedSource = module.source;

      // 替换 require 调用中的相对路径为绝对路径
      module.dependencies.forEach((dep) => {
        const relativePath = path.relative(path.dirname(module.filePath), dep);
        const patterns = [
          `require("./${relativePath}")`,
          `require('./${relativePath}')`,
          `require("./${relativePath.replace(/\.js$/, '')}")`,
          `require('./${relativePath.replace(/\.js$/, '')}')`,
        ];

        patterns.forEach((pattern) => {
          processedSource = processedSource.replace(
            pattern,
            `require("${dep}")`
          );
        });
      });

      modules += `"${module.id}": function(module, exports, require) {\n${processedSource}\n},\n`;
    });

    return `
(function(modules) {
  // 模块缓存
  var installedModules = {};
  
  // require 函数实现
  function __webpack_require__(moduleId) {
    // 检查缓存
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    
    // 创建新模块
    var module = installedModules[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {}
    };
    
    // 执行模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    // 标记为已加载
    module.loaded = true;
    
    // 返回模块导出
    return module.exports;
  }
  
  // 入口模块执行
  return __webpack_require__("${path.resolve(this.config.entry)}");
})({
${modules}
})`;
  }
}

module.exports = MiniWebpack;

用一个例子来验证 MiniWebpack 的实现

完整示例项目github.com/frontzhm/mi...

让我们用一个具体的例子来验证 MiniWebpack 的实现。这个例子包含多个模块和依赖关系:

项目结构

bash 复制代码
mini-webpack/example/
├── src/
│   ├── index.js              # 入口文件
│   ├── utils/
│   │   ├── helpers.js         # 工具函数模块
│   │   └── ui.js             # UI 工具模块
│   ├── styles/
│   │   └── main.css          # 样式文件
│   └── components/
│       └── ComponentA.js     # 组件模块
├── dist/
│   └── bundle.js             # 构建输出
└── index.html                # HTML 模板

源码内容

入口文件 (src/index.js)

javascript 复制代码
// 入口文件
import { greet } from './utils/helpers';
import { createButton } from './utils/ui';
import './styles/main.css';

console.log('🚀 Mini Webpack 示例项目启动!');

// 使用工具函数
const message = greet('Mini Webpack');
console.log(message);

// 创建按钮
const button = createButton('点击我!');
document.body.appendChild(button);

// 动态导入示例
import('./components/ComponentA').then(module => {
  console.log('动态导入成功:', module.default);
});

工具函数模块 (src/utils/helpers.js)

javascript 复制代码
// 工具函数模块
export function greet(name) {
  return `Hello, ${name}! 欢迎使用 Mini Webpack!`;
}

export function formatDate(date) {
  return new Date(date).toLocaleDateString('zh-CN');
}

export function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

UI 工具模块 (src/utils/ui.js)

javascript 复制代码
// UI 工具模块
export function createButton(text) {
  const button = document.createElement('button');
  button.textContent = text;
  button.style.cssText = `
    padding: 10px 20px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
  `;
  
  button.addEventListener('click', () => {
    alert('按钮被点击了!');
  });
  
  return button;
}

export function createCard(title, content) {
  const card = document.createElement('div');
  card.style.cssText = `
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 20px;
    margin: 10px 0;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  `;
  
  card.innerHTML = `
    <h3>${title}</h3>
    <p>${content}</p>
  `;
  
  return card;
}

样式文件 (src/styles/main.css)

css 复制代码
/* 主样式文件 */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  margin: 0;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
}

.container {
  max-width: 800px;
  margin: 0 auto;
  background: white;
  border-radius: 10px;
  padding: 30px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}

h1 {
  color: #333;
  text-align: center;
  margin-bottom: 30px;
}

button {
  transition: all 0.3s ease;
}

button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 20px;
  margin: 10px 0;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

组件模块 (src/components/ComponentA.js)

javascript 复制代码
// 组件 A
export default class ComponentA {
  constructor(name) {
    this.name = name;
    this.element = null;
  }

  render() {
    this.element = document.createElement('div');
    this.element.innerHTML = `
      <h2>组件 A: ${this.name}</h2>
      <p>这是一个动态导入的组件</p>
      <button id="component-btn">组件按钮</button>
    `;
    
    // 添加事件监听器
    this.element.querySelector('#component-btn').addEventListener('click', () => {
      console.log('组件按钮被点击了!');
    });
    
    return this.element;
  }

  mount(container) {
    if (this.element) {
      container.appendChild(this.element);
    }
  }
}

构建过程

运行 npm run build 后,MiniWebpack 会:

  1. 解析入口文件 :从 src/index.js 开始

  2. 提取依赖 :发现对 utils/helpers.jsutils/ui.jsstyles/main.css 的依赖

  3. 递归解析:解析每个依赖模块

  4. 生成依赖图

    bash 复制代码
    src/index.js → utils/helpers.js
                 → utils/ui.js  
                 → styles/main.css
                 → components/ComponentA.js

生成的 Bundle 结构

构建后的 dist/bundle.js 包含:

javascript 复制代码
(function(modules) {
  // 模块缓存
  var installedModules = {};
  
  // require 函数实现
  function __webpack_require__(moduleId) {
    // 检查缓存
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    
    // 创建新模块
    var module = installedModules[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {}
    };
    
    // 执行模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    // 标记为已加载
    module.loaded = true;
    
    // 返回模块导出
    return module.exports;
  }
  
  // 入口模块执行
  return __webpack_require__("/Users/.../src/index.js");
})({
  "/Users/.../src/index.js": function(module, exports, require) {
    var _helpers = require("/Users/.../src/utils/helpers.js");
    var _ui = require("/Users/.../src/utils/ui.js");
    require("/Users/.../src/styles/main.css");
    
    console.log('🚀 Mini Webpack 示例项目启动!');
    
    // 使用工具函数
    var message = (0, _helpers.greet)('Mini Webpack');
    console.log(message);
    
    // 创建按钮
    var button = (0, _ui.createButton)('点击我!');
    document.body.appendChild(button);
    
    // 动态导入示例
    Promise.resolve().then(function () {
      return _interopRequireWildcard(require('./components/ComponentA'));
    }).then(function (module) {
      console.log('动态导入成功:', module["default"]);
    });
  },
  
  "/Users/.../src/utils/helpers.js": function(module, exports, require) {
    // 工具函数实现
    function greet(name) {
      return "Hello, " + name + "! 欢迎使用 Mini Webpack!";
    }
    
    function formatDate(date) {
      return new Date(date).toLocaleDateString('zh-CN');
    }
    
    function debounce(func, delay) {
      var timeoutId;
      return function () {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(function () {
          return func.apply(this, arguments);
        }, delay);
      };
    }
    
    exports.greet = greet;
    exports.formatDate = formatDate;
    exports.debounce = debounce;
  },
  
  // ... 其他模块
});

运行效果

在浏览器中打开 index.html,会看到:

  1. 控制台输出

    复制代码
    🚀 Mini Webpack 示例项目启动!
    Hello, Mini Webpack! 欢迎使用 Mini Webpack!
  2. 页面效果

    • 渐变背景
    • 白色容器卡片
    • 蓝色按钮(点击会弹出提示)
    • 动态导入的组件

相关资源

相关推荐
aesthetician3 小时前
Node.js 24.10.0: 拥抱现代 JavaScript 与增强性能
开发语言·javascript·node.js
haidragon3 小时前
第 1 周 —— **OSI 之旅开始了**
前端
Python私教3 小时前
React 19 如何优雅整合 Ant Design v5 与 Tailwind CSS v4
前端·css·react.js
拳打南山敬老院3 小时前
🚀 为什么 LangChain 不做可视化工作流?从“工作流”到“智能体”的边界与融合
前端·人工智能·后端
前端老鹰3 小时前
解锁 JavaScript 字符串补全魔法:padStart()与 padEnd()
前端·javascript
刺客_Andy3 小时前
React 第四十一节Router 中 useActionData 使用方法案例以及注意事项
前端·react.js
一心只读圣贤书3 小时前
解决.spec-workflow-mcp配置报错
前端
日月之行_4 小时前
还在用ref操作DOM?Vue 3.5 useTemplateRef如何彻底改变DOM引用方式
前端
漫天星梦4 小时前
简约版3D地球实现,多框架支持
前端·vue.js