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. 循环依赖处理
    提前缓存部分导出的对象(未初始化状态)。
相关推荐
牧码岛14 小时前
服务端之NestJS接口响应message编写规范详解、写给前后端都舒服的接口、API提示信息标准化
服务器·后端·node.js·nestjs
嚴寒1 天前
Node 版本管理还在手动重装全局包?这个方案让你效率翻倍
node.js
你的人类朋友2 天前
【Node】单线程的Node.js为什么可以实现多线程?
前端·后端·node.js
HoJunjie2 天前
macOS sequoia 15.7.1 源码安装node14,并加入nvm管理教程
macos·node.js
做运维的阿瑞3 天前
Windows 环境下安装 Node.js 和 Vue.js 框架完全指南
前端·javascript·vue.js·windows·node.js
你的人类朋友3 天前
【Node】Node.js 多进程与多线程:Cluster 与 Worker Threads 入门
前端·后端·node.js
谢尔登3 天前
【Nest】日志记录
javascript·中间件·node.js
HWL56793 天前
输入框内容粘贴时   字符净化问题
前端·vue.js·后端·node.js
. . . . .4 天前
Node.js 的替代品Bun
node.js