如何构建模块打包器:8 大核心技术完整指南及代码示例

发布: (2025年12月5日 GMT+8 18:50)
5 min read
原文: Dev.to

Source: Dev.to

模块解析

解析器从入口文件(例如 index.js)开始,确定每个 import 的确切文件位置。
对于相对路径,它会将请求与导入文件所在的目录拼接;对于包名,它会向上遍历目录树寻找 node_modules 文件夹,读取包的 package.json 以找到主入口,并尝试常见的文件扩展名。

// A simplified look at the resolution logic
async resolve(request, fromDir) {
  // Is it a direct path?
  if (request.startsWith('.')) {
    let candidate = path.resolve(fromDir, request);
    // Does it have an extension? If not, add one.
    if (!path.extname(candidate)) {
      candidate = await this.tryWithExtensions(candidate);
    }
    return candidate;
  }

  // It's a package name, look in node_modules
  let dir = fromDir;
  while (dir !== path.parse(dir).root) {
    const pkgPath = path.join(dir, 'node_modules', request);
    if (await this.exists(pkgPath)) {
      // Found the package folder, now find its main file
      return this.resolvePackageMain(pkgPath);
    }
    // Go up one directory and check again
    dir = path.dirname(dir);
  }
  throw new Error(`Module not found: ${request}`);
}

构建依赖图

为了了解模块之间的相互依赖,我们将每个文件解析成抽象语法树(AST)。AST 让我们能够可靠地定位 importexport 语句。

const parser = require('@babel/parser');
const code = `import { formatDate } from './dateUtils.js';`;
const ast = parser.parse(code, { sourceType: 'module' });

console.log(ast.program.body[0].type); // 'ImportDeclaration'
console.log(ast.program.body[0].source.value); // './dateUtils.js'

随后遍历 AST 并收集依赖:

const traverse = require('@babel/traverse').default;
const dependencies = [];

traverse(ast, {
  ImportDeclaration(path) {
    // Record where this import is pointing to
    dependencies.push(path.node.source.value);
  },
  ExportNamedDeclaration(path) {
    // Also check for re-exports: export { x } from './module'
    if (path.node.source) {
      dependencies.push(path.node.source.value);
    }
  }
});
// Now 'dependencies' contains ['./dateUtils.js']

结果是每个文件及其所需模块的映射,形成依赖图。

转换 Import 与 Export

打包需要把 ES 模块语法转换为运行时能够理解的格式。Import 会被转化为对自定义 __require__ 函数的调用,Export 则附加到 module.exports 上。

转换 Import

const t = require('@babel/types');

// Suppose we know the './stringLib' module has ID = 2
const moduleId = 2;

// Create the call to our runtime function: __require__(2)
const requireCall = t.callExpression(
  t.identifier('__require__'),
  [t.numericLiteral(moduleId)]
);

// Create the destructuring: const { capitalize } = __require__(2);
const newDeclaration = t.variableDeclaration('const', [
  t.variableDeclarator(
    t.objectPattern([
      t.objectProperty(
        t.identifier('capitalize'),
        t.identifier('capitalize'),
        false,
        true // shorthand
      )
    ]),
    requireCall
  )
]);

// 'path' is the AST node for the import statement
path.replaceWith(newDeclaration);

转换 Export

Export 会被改写为给 module.exports 赋值:

// Example: export const version = '1.0';
module.exports.version = '1.0';

包装模块

每个已转换的模块都会被包装在一个函数中,以提供私有作用域,并向内部代码暴露 moduleexports__require__

function (module, exports, __require__) {
  // The transformed module code lives here
  const { capitalize } = __require__(2);
  module.exports.greet = (name) => capitalize(name);
}

运行时加载器

打包文件以一个小型运行时结束,该运行时将所有模块包装函数存放在数组中,并提供 __require__ 函数按需加载模块。它还会缓存已执行的模块,以处理循环依赖。

// The final bundle output starts with this runtime
(function (modules) {
  // A cache for modules that have already been executed
  const moduleCache = [];

  // The require function our transformed code will call
  function __require__(moduleId) {
    // Return cached module if it exists
    if (moduleCache[moduleId]) {
      return moduleCache[moduleId].exports;
    }

    // Create a new module object for this id
    const module = { exports: {} };
    // Store it in cache immediately to handle circular dependencies
    moduleCache[moduleId] = module;

    // Get the module's wrapper function and execute it
    // It receives the module object, its exports, and the __require__ function
    modules[moduleId].call(module.exports, module, module.exports, __require__);

    // Return the now-populated exports
    return module.exports;
  }

  // Start the application by requiring the entry module (id 0)
  __require__(0);
})([
  // Our modules registry array starts here
  // Each element is a wrapped module function
  function (module, exports, __require__) {
    // Transformed code for module ID 0 (entry point)
    const utils = __require__(1);
    console.log(utils);
  },
  // ...more wrapped modules...
]);
Back to Blog

相关文章

阅读更多 »

我在 JavaScript 的第一步:简要解析

JavaScript 中的变量 **let** 用于可以在以后更改的值。 ```javascript let age = 20; age = 21; ``` **const** 用于不应被更改的值。 ```javascript const PI = 3.14159; ```