CommonJS 模块化的实现源码解析

理解 Node.js 中 CommonJS 模块化的实现源码需要深入其核心机制。以下是基于 Node.js 源码的详细解析,涵盖关键模块和函数:

核心文件

  1. lib/internal/modules/cjs/loader.js
    CommonJS 模块系统的核心实现。
  2. lib/internal/modules/run_main.js
    处理入口模块的执行。
  3. lib/internal/modules/package_json_reader.js
    读取 package.json"main" 字段。

关键概念与源码解析

1. Module 类:模块的抽象表示

javascript 复制代码
// lib/internal/modules/cjs/loader.js
class Module {
  constructor(id = "", parent) {
    this.id = id;          // 模块唯一标识(通常是绝对路径)
    this.path = path.dirname(id); // 模块所在目录
    this.exports = {};     // 模块导出的对象
    this.parent = parent;  // 父模块(引入者)
    this.filename = null;  // 模块文件绝对路径
    this.loaded = false;   // 是否已加载完成
    this.children = [];    // 子模块(该模块引入的模块)
  }

  // 加载模块的核心方法
  load(filename) {
    this.filename = filename;
    // 获取文件扩展名(.js, .json, .node)
    const extension = findLongestRegisteredExtension(filename);
    // 根据扩展名调用对应加载器
    Module._extensions[extension](this, filename);
    this.loaded = true;
  }

  // 编译模块源码
  _compile(content, filename) {
    // 1. 将模块代码包裹在函数中
    const compiledWrapper = wrapSafe(filename, content);
    // 2. 准备参数:exports, require, module, __filename, __dirname
    const dirname = path.dirname(filename);
    const require = makeRequireFunction(this);
    const exports = this.exports;
    const thisValue = exports;
    // 3. 执行包裹函数
    compiledWrapper.call(
      thisValue,
      exports,
      require,
      this,
      filename,
      dirname
    );
  }
}

2. 模块包装函数

源码通过函数包裹模块代码,注入作用域变量:

javascript 复制代码
// lib/internal/modules/cjs/loader.js
const wrapper = [
  "(function (exports, require, module, __filename, __dirname) { ",
  "\n});"
];

function wrapSafe(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
}

最终执行的代码形式:

javascript 复制代码
(function (exports, require, module, __filename, __dirname) {
  // 用户模块代码
});

3. require() 函数的实现

javascript 复制代码
// lib/internal/modules/cjs/loader.js
Module.prototype.require = function (id) {
  // 1. 解析模块路径
  const filename = Module._resolveFilename(id, this);
  // 2. 检查缓存
  const cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }
  // 3. 创建新模块实例
  const module = new Module(filename, this);
  // 4. 缓存模块(防止重复加载)
  Module._cache[filename] = module;
  // 5. 加载模块
  try {
    module.load(filename);
  } catch (err) {
    delete Module._cache[filename]; // 加载失败移除缓存
    throw err;
  }
  // 6. 返回模块的 exports 对象
  return module.exports;
};

4. 路径解析算法

javascript 复制代码
// lib/internal/modules/cjs/loader.js
Module._resolveFilename = function (request, parent) {
  // 1. 检查核心模块(如 'fs')
  if (NativeModule.canBeRequiredByUsers(request)) {
    return request;
  }
  // 2. 解析相对/绝对路径
  let filename;
  if (path.isAbsolute(request)) {
    filename = path.resolve(request);
  } else {
    // 从父模块目录向上递归查找 node_modules
    filename = resolveLookupPaths(request, parent);
  }
  // 3. 检查文件扩展名(.js, .json, .node)
  filename = tryExtensions(filename);
  return filename;
};

关键辅助函数:

  • resolveLookupPaths():递归查找 node_modules
  • tryExtensions():尝试添加扩展名(['.js', '.json', '.node'])。

5. 文件加载器(Module._extensions

javascript 复制代码
// lib/internal/modules/cjs/loader.js
Module._extensions = {
  '.js'(module, filename) {
    // 1. 读取文件内容
    const content = fs.readFileSync(filename, 'utf8');
    // 2. 编译执行
    module._compile(content, filename);
  },
  '.json'(module, filename) {
    // 直接解析 JSON 文件
    const content = fs.readFileSync(filename, 'utf8');
    module.exports = JSON.parse(stripBOM(content));
  },
  '.node'(module, filename) {
    // 加载 C++ 插件
    return process.dlopen(module, filename);
  }
};

6. 循环依赖的处理

当模块 A 加载 B,B 又加载 A 时:

javascript 复制代码
// 模块 A: a.js
exports.a = 1; // ✅ 此时已导出
const b = require('./b');
console.log(b); // { b: 2 }
exports.a = 3; // ✅ 更新导出

// 模块 B: b.js
exports.b = 2;
const a = require('./a');
console.log(a); // { a: 1 } ❗ 此时 a 尚未完全加载
exports.b = 4;

源码实现关键

  • 模块在 load() 前就被加入缓存(Module._cache)。
  • 当 B 加载 A 时,A 的 exports 对象已存在(但未完全初始化),B 获取到部分导出的对象。

核心流程总结

  1. require(id) 调用

    用户代码触发 require()

  2. 路径解析
    Module._resolveFilename()id 转为绝对路径。

  3. 缓存检查

    若模块已缓存,直接返回 cachedModule.exports

  4. 创建模块实例
    new Module(filename, parent)

  5. 缓存模块
    Module._cache[filename] = module

  6. 加载模块
    module.load(filename)

    • 根据扩展名调用对应加载器(.js/.json/.node)。
    • .js 文件:读取源码 → 包裹函数 → 编译执行。
  7. 执行模块代码

    包裹函数注入 exports, require, module, __filename, __dirname

  8. 返回 exports 对象

    用户得到模块导出的内容。

关键设计亮点

  1. 缓存机制
    Module._cache 对象存储所有加载过的模块,避免重复加载。
  2. 同步加载
    fs.readFileSync() 确保模块按顺序加载。
  3. 作用域隔离
    包裹函数创建闭包,避免全局污染。
  4. 扩展名支持
    通过 Module._extensions 支持多种文件类型。
  5. 循环依赖处理
    提前缓存部分导出的对象(未初始化状态)。
相关推荐
水冗水孚26 分钟前
🚀四种方案解决浏览器地址栏预览txt文本乱码问题🚀Content-Type: text/plain;没有charset=utf-8
javascript·nginx·node.js
程序猿小D20 小时前
第29节 Node.js Query Strings
node.js·vim·express
前端服务区21 小时前
发布npm包
node.js
sg_knight1 天前
Rollup vs Webpack 深度对比:前端构建工具终极指南
前端·javascript·webpack·node.js·vue·rollup·vite
啃火龙果的兔子1 天前
在服务器上使用 Docker 部署 Node.js 后端服务和前端项目
服务器·docker·node.js
Smile_Gently1 天前
Mac 系统 Node.js 安装与版本管理指南
macos·node.js
a别念m1 天前
webpack基础与进阶
前端·webpack·node.js
qq_12498707531 天前
基于Node.js的线上教学系统的设计与实现(源码+论文+调试+安装+售后)
java·spring boot·后端·node.js·毕业设计
程序猿小D1 天前
第28节 Node.js 文件系统
服务器·前端·javascript·vscode·node.js·编辑器·vim