Node.js 模块系统详解:从 CommonJS 到 ES Module

作者:墨池时间:2026-10-23 21:24:25浏览:606

本文深入解析 Node.js 模块系统,涵盖 CommonJS 规范下的 module.exports 与 require 用法、模块加载优先级及缓存机制。详细阐述循环依赖的执行顺序与原理,并介绍 Node.js 从 v12 开始对 ES Module 的原生支持及配置方法,帮助开发者掌握模块管理的核心知识。

Node.js 模块系统概述

Node.js 的模块

JavaScript 最初作为网页交互脚本语言诞生时,并未内置模块系统。随着应用复杂度提升,单文件加函数划分的方式已无法满足需求。ES6 引入了 class 和 module,提供了类似高级语言的代码组织方式。


import _ from 'lodash';

class Fun {}

export default Fun;

上述代码展示了模块系统的两个核心要素:export用于定义对外接口,import用于引入其他模块的功能。

CommonJS 规范与接口导出

在 ES6 普及前,社区主要存在 CommonJS 和 AMD 两种加载方案。Node.js 诞生早于 ES6,采用了类似 CommonJS 的实现,并遵循以下原则:

1. 每个文件视为独立模块,内部变量作用域局限于该文件内。 2. 通过 module.exports 对象导出对外接口。 3. 使用 require 引入其他模块。

circle.js


const { PI } = Math;

module.exports = function area(r) {
 PI * r ** 2;
};

上述代码实现了一个 Node.js 模块,该模块未依赖其他模块,仅导出一个计算圆面积的 area 方法。

test.js


const area = require('./circle.js');
console.log(`半径为 4 的圆的面积是 ${area(4)}`);

该模块依赖 circle.js,通过调用其导出的 area 方法计算并输出半径为 4 的圆面积。

module.exports

模块通过 module.exports 暴露接口,常见用法包括为其添加属性或直接赋值给新对象。

test.js


// 添加属性
module.exports.prop1 = xxx;
module.exports.funA = xxx;
module.exports.funB = xxx;

// 赋值到全新对象
module.exports = {
 prop1,
 funA,
 funB,
};

这两种写法在功能上等价,实际使用时无明显区别。


const mod = require('./test.js');

console.log(mod.prop1);
console.log(mod.funA());

此外还可以直接使用 exports 对象添加属性,但不能直接赋值给新对象,原因将在后文解释。


// 正确的写法:添加属性
exports.prop1 = xxx;
exports.funA = xxx;
exports.funB = xxx;

// 赋值到全新对象
module.exports = {
 prop1,
	funA,
 funB,
};

require('id')

模块加载机制与路径解析

模块类型

require 的 id 参数支持模块名和文件路径两种形式。

模块名


const fs = require('fs');
const _ = require('lodash');

示例中的 fs 和 lodash 均为模块名。fs 是 Node.js 内置核心模块,lodash 是通过 npm 安装的第三方模块。若出现重名,优先加载系统内置模块。

由于项目中可能存在多个 node_modules 文件夹(Node.js 的设计特性),第三方模块查找遵循就近原则逐层向上回溯(可通过打印 module.paths 查看具体路径),直至 NODE_PATH 环境变量指定的文件系统根目录。详细过程可参考官方文档。

此外,Node.js 还会搜索以下全局目录:

  • $HOME/.node_modules
  • $HOME/.node_libraries
  • $PREFIX/lib/node

其中 $HOME 为用户主目录,$PREFIX 为 Node.js 配置的 node_prefix。强烈建议将所有依赖置于本地 node_modules 目录,以获得更快的加载速度和更高的可靠性。

文件路径

模块也可通过文件路径加载,这是项目内自定义模块的通用方式。路径可省略扩展名,Node.js 会按 .js、.json、.node 的顺序尝试解析。

  • 以 '/' 为前缀的模块表示绝对路径,按系统路径查找。
  • 以 './' 为前缀的模块表示相对于当前调用 require 方法文件的路径,不受后续模块使用位置的影响。

模块缓存与循环依赖处理

单次加载 & 循环依赖

模块在首次加载后会被缓存到 Module._cache ,若每次调用 require('foo') 均解析至同一文件,则返回相同对象,且多次调用 require(foo) 不会致使模块代码被执行多次。Node.js 依据实际文件名缓存模块,故从不同层级目录引用相同模块不会重复加载。

理解单次加载机制有助于理解模块循环依赖时的行为。

a.js


console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js


console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js


console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

当 main.js 加载 a.js 时,a.js 内部又加载 b.js,此时 b.js 尝试加载 a.js。

为防止无限循环,Node.js 会返回 a.js 的 exports 对象的未完成副本给 b.js。b.js 加载完成后,将完整的 exports 对象提供给 a.js。

因此示例的输出结果为:

main 开始 a 开始 b 开始 在 b 中,a.done = false b 结束 在 a 中,b.done = true a 结束 在 main 中,a.done=true,b.done=true

若对上述过程感到困惑,无需深究,因为日常开发中应避免使用循环依赖。

模块工作原理与封装器

工作原理

Node.js 中每个文件是一个模块,内部变量为局部变量,不污染全局。执行模块代码前,Node.js 会使用如下函数封装器进行封装:


(function(exports, require, module, __filename, __dirname) {
	// 模块的代码实际上在这里
});
  • __filename:当前模块文件的绝对路径
  • __dirname:当前模块文件所在目录的绝对路径
  • module:当前的模块实例
  • require:加载其他模块的方法,是 module.require 的快捷方式
  • exports:导出模块接口的对象,是 module.exports 的快捷方式

回顾前文问题:为什么 exports 对象不支持赋值为其他对象?在上述函数中添加一句 exports 的来源定义即可明了:


const exports = module.exports;
(function(exports, require, module, __filename, __dirname) {
	// 模块的代码实际上在这里
});

其他模块 require 获取的始终是 module.exports 对象。若将 exports 直接赋值给新对象,将断开与 module.exports 的连接,导致导出失效。

ES Module 在 Node.js 中的支持

在 Node.js 中使用 ES Module

随着 ES6 的普及,Node.js 也支持了 ES6 Module,主要有以下几种方式:

babel 构建

在 v12 之前版本,使用 babel 构建是最简单通用的方式,具体配置参考 @babel/preset-env。

.babelrc


{
 "presets": [
  ["@babel/preset-env", {
   "targets": {
    "node": "8.9.0",
    "esmodules": true
   }   
  }]
 ]
}

原生支持

在 v12 之后,可以使用原生方式支持 ES Module。

  1. 开启 --experimental-modules
  2. 将模块名修改为 .mjs (强烈不推荐使用)或在 package.json 中设置 "type": module

这样 Node.js 会将 js 文件视为 ES Module 处理,更多详情参考官方文档。

以上就是谈谈node.js中的模块系统的详细内容,更多关于node.js 模块的资料请关注本站其它相关文章!

作者最新文章