从零实现Webpack模块解析器:核心流程与代码解析

作者:构建工坊

时间:2026-10-22

浏览:595

本文基于Webpack源码梳理,详细解析如何从零手写一个最基础的模块解析器。通过读取入口文件、转换AST语法树、深度遍历提取依赖、转换可执行代码以及模拟require函数执行,完整展示了模块打包的核心流程。文章包含createAsset、createGraph和bundle三个关键函数的代码实现,帮助开发者深入理解Webpack内部机制。

文章目录

近期在研读Webpack源码时,鉴于其复杂性,作者先对整体流程进行了梳理,并参照官方示例,尝试编写了一个最基础的Webpack模块解析器。

该示例代码量精简,完整实现已托管至GitHub,仓库名为“手写webpack模块解析器”。

模块解析器整体流程

接下来对整体处理流程进行详细分析。

第一步是读取入口文件的内容。

第二步是将读取到的文件内容转换为AST(抽象语法树)。

第三步是对生成的语法树进行深度遍历,从中提取所有模块依赖项,并将它们添加到一个依赖数组中。

第四步是将处理后的AST代码重新转换回浏览器可执行的JavaScript代码。

第五步是编写模拟的require函数,该函数根据入口文件自动执行所有已识别的依赖模块。

最后一步是输出最终的运行结果。

核心函数实现解析

实现这一流程的核心在于createAsset函数,负责单个模块的解析。


// 读取内容并提取它的依赖关系
function createAsset(filename) {
 // 以字符串的形式读取文件
 const content = fs.readFileSync(filename, "utf-8");

 // 转换字符串为ast抽象语法树
 const ast = babylon.parse(content, {
  sourceType: "module"
 });

 const dependencies = [];

 // 遍历抽象语法树
 traverse(ast, {
  // 每当遍历到import语法的时候
  ImportDeclaration: ({ node }) => {
   // 把依赖的模块加入到数组中
   dependencies.push(node.source.value);
  }
 });

 const id = ID++;

 // 转换为浏览器可运行的代码
 const { code } = babel.transformFromAstSync(ast, null, {
  presets: ["@babel/preset-env"]
 });

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

随后是createGraph函数,用于构建完整的依赖关系图。


// 从入口开始,分析所有依赖项,形成依赖图,采用深度优先遍历
function createGraph(entry) {
 const mainAsset = createAsset(entry);

 // 定义一个保存依赖项的数组
 const queue = [mainAsset];

 for (const asset of queue) {
  const dirname = path.dirname(asset.filename);

  // 定义一个保存子依赖项的属性
  asset.mapping = {};

  asset.dependencies.forEach(relativePath => {
   const absolutePath = path.join(dirname, relativePath);

   const child = createAsset(absolutePath);

   // 给子依赖项赋值
   asset.mapping[relativePath] = child.id;

   // 将子依赖也加入队列中,循环处理
   queue.push(child);
  });
 }
 return queue;
}

最后是bundle函数,根据依赖图生成最终的打包文件。


// 根据生成的依赖关系图,生成浏览器可执行文件
function bundle(graph) {
 let modules = "";

 // 把每个模块中的代码放在一个function作用域内
 graph.forEach(mod => {
  modules += `${mod.id}:[
   function (require, module, exports){
    ${mod.code}
   },
   ${JSON.stringify(mod.mapping)},
  ],`;
 });

 // require, module, exports 不能直接在浏览器中使用,这里模拟了模块加载,执行,导出操作。
 const result = `
  (function(modules){
   // 创建一个require()函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它.
   function require(id){
    const [fn, mapping] = modules[id];

    function localRequire(relativePath){
     // 根据mapping的路径,找到对应的模块id
     return require(mapping[relativePath]);
    }

    const module = {exports:{}};

    // 执行转换后的代码,并输出内容。
    fn(localRequire,module,module.exports);

    return module.exports;
   }

   // 执行入口文件
   require(0);

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

 return result;
}

代码执行细节

以下是具体的代码执行与解析部分。


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

以上内容即为本文的全部讲解,希望能为各位的学习提供有益参考,也欢迎大家继续支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章