Node.js支持的两种JavaScript模块系统: CommonJS和ECMAScript模块(ES模块)。
CommonJS模块是Node.js最初采用的模块系统,它为Node.js提供了一种组织和打包JavaScript代码的方式。
通过这种模块系统,开发者可以将代码分割成不同的模块,每个模块都有自己的作用域,模块之间可以通过特定的语法进行相互引用。
例如,使用require()函数来加载其他模块,使用module.exports来导出模块的功能。
注意:CommonJS模块即将废弃,ES模块是未来的标准,Node.js将在未来的版本中支持ES模块。
本文仅作为学习备忘。
模块的基本概念
以下概念及测试代码仅限于 Node.js CommonJS modules。
在Node.js中,每个文件都被视为一个模块。
模块是一个包含JavaScript代码的文件,它可以包含变量、函数、类等。
每个 Node.js 文件(模块)在运行时会被包装成一个函数,函数参数中会传入 module
对象。它代表当前模块的元信息。
module
对象有以下属性:
module.id
:模块的标识符,通常是模块文件的路径。module.filename
:模块文件的完整路径。module.paths
:模块搜索路径数组。module.parent
:父模块的引用。module.children
:子模块的引用数组。module.exports
:模块的导出对象。
我们可以创建一个 demo1.cjs 文件,在其中输出 module
对象的及属性:
javascript
// demo1.cjs
console.log(module);
输出结果如下:
javascript
{
id: '.',
path: 'D:\\itshizhan-developer\\webfront-apprentice\\nodejs-all\\\src',
exports: {},
filename: 'D:\\itshizhan-developer\\webfront-apprentice\\nodejs-all\\\src\\test.cjs',
loaded: false,
children: [],
paths: [
'D:\\itshizhan-developer\\webfront-apprentice\\nodejs-all\\\src\\node_modules',
'D:\\itshizhan-developer\\webfront-apprentice\\nodejs-all\\\node_modules',
'D:\\itshizhan-developer\\webfront-apprentice\\nodejs-all\\node_modules',
'D:\\itshizhan-developer\\webfront-apprentice\\node_modules',
'D:\\itshizhan-developer\\node_modules',
'D:\\node_modules'
]
}
module.exports
module.exports
是 Node.js 中用于导出模块的对象。
它允许我们将模块中的变量、函数或类等导出,以便其他模块可以使用。
默认情况下,module.exports 是一个空对象 {}
, 我们可以将需要导出的内容赋值给它。
其他模块通过 require()
引入该模块时,实际获取的是 module.exports 的值
。
例如:假如有一个 a.cjs 文件,内容如下:
javascript
// a.cjs
function add(a,b) {
console.log(a+b)
}
在 a.cjs 中,我们没有使用 module.exports 导出 add 函数,而是直接在文件中定义了一个函数。
在其他模块中,我们可以通过 require() 引入 a.cjs 文件, 获取的默认就是 module.exports 的默认值
,即空对象{}
。
例如:
在 b.cjs 文件中,我们可以通过 require() 引入 a.cjs 文件:
javascript
// b.cjs
const a = require('./a.cjs');
console.log(a);
输出结果如下:
javascript
{}
如果要想使用 a.cjs 中的 add 函数,我们可以将 add 函数赋值给 module.exports:
javascript
// a.cjs
function add(a,b) {
console.log(a+b)
}
module.exports = add;
在 b.cjs 文件中,我们可以通过 require() 引入 a.cjs 文件,获取的是 add 函数:
javascript
// b.cjs
const a = require('./a.cjs');
a(2,3) // 结果5
上述实际上是将函数直接导出为模块
将函数或类作为对象属性导出
加入有文件c.cjs
,内容如下:
javascript
// c.cjs
function add(a,b) {
console.log(a+b)
}
class Person {
constructor(name,age) {
this.name = name;
this.age = age;
}
log() {
console.log(this.name,this.age)
}
}
const mName = 'c.cjs'
module.exports = {
add,
Person,
mName
}
在其他模块中,我们可以通过 require() 引入 c.cjs 文件,获取的是一个对象,该对象包含了 add 函数、Person 类和 mName 变量:
javascript
// d.cjs
const c = require('./c.cjs');
console.log(c);
c.add(1,2);
const p = new c.Person('zhangsan',18);
p.log();
console.log(c.mName);
输出结果如下:
javascript
{ add: [Function: add], Person: [class Person], mName: 'c.cjs' }
3
zhangsan 18
c.cjs
导出多个对象
javascript
// 方式1:直接赋值对象
module.exports = { add, subtract };
// 方式2:动态添加属性
module.exports.add = (a, b) => a + b;
module.exports.subtract = (a, b) => a - b;
多次使用 module.exports
如果在一个模块中多次使用 module.exports,只有最后一次的赋值会生效。
exports 变量
本质:exports 是 module.exports 的一个引用。
初始时,exports === module.exports(即 exports 指向 module.exports 的内存地址)。
正确用法:通过 exports 添加属性(因为直接操作引用)。
错误用法:直接给 exports 赋值会切断它与 module.exports 的关联。
javascript
// ✅ 正确:添加属性(操作引用)
exports.name = "Node.js";
exports.version = "20.x";
// 等价于:
module.exports.name = "Node.js";
module.exports.version = "20.x";
// ❌ 错误:直接赋值(切断关联)
exports = { name: "Node.js" }; // 此时 exports 不再指向 module.exports
关键规则:
✅ 最终导出的内容始终是 module.exports
。
✅ 如果同时操作 exports 和 module.exports,以 module.exports 为准。
✅ 统一使用 module.exports。避免混淆, 不建议使用单独使用 exports 。
require
上文已经使用,require 是用于导入其他模块的核心函数。
它接受一个模块标识符作为参数,返回被导入模块的导出对象。
模块标识符可以是相对路径或绝对路径,也可以是 Node.js 内置模块的名称。
require 本身用于导入整个模块,但可以通过解构赋值直接获取模块导出对象的特定属性。
例如:
javascript
// math.js(导出一个对象)
module.exports = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
PI: 3.14
};
// main.js(通过解构提取属性)
const { add, PI } = require('./math');
console.log(add(2, 3)); // 5
console.log(PI); // 3.14
也可以先导入整个模块对象,再通过点号(.)访问其属性:
javascript
// main.js(先导入整个对象,再访问属性)
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.PI); // 3.14
模块加载机制
在 Node.js 中,模块的加载是通过 require() 函数实现的。
require() 函数会根据模块的路径来加载模块,并返回模块的导出对象。
模块加载的过程如下:
-
解析模块路径:根据模块标识符(路径)解析模块文件。
-
缓存加载:检查模块是否已经被加载过,如果是,则直接返回缓存的模块。
-
创建模块对象:创建一个新的模块对象,用于存储模块的状态和导出对象。
-
执行模块代码:加载模块文件的内容,并执行模块代码。
-
导出模块:根据模块代码中的导出语句,将模块的导出对象赋值给 module.exports。
-
返回导出对象:返回 module.exports 的值,作为 require() 函数的返回值。
✅ 例如:
当通过 require('./myModule') 加载模块时:
Node.js 会执行该模块的代码。
返回该模块的 module.exports 值。
模块的代码只会执行一次(后续 require() 会从缓存中读取 module.exports)。
javascript
// myModule.cjs
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count
};
console.log("count=",count);
在e.cjs 中导入myModule.cjs模块:
javascript
const { increment, getCount } = require('./myModule.cjs');
increment();
console.log(getCount()); // 输出 1
输出结果如下:
javascript
// 加载模块后,会执行该模块的代码
count= 0
1
模块查找规则
当使用 require('module')
时,Node.js 按以下顺序查找模块:
(1)核心模块
优先检查是否为 Node.js 内置模块(如 fs
、path
),如果是则直接加载。
(2)文件模块
- 精确路径:
javascript
require('./utils.js'); // 加载当前目录下的 utils.js 文件
- 省略扩展名:
javascript
require('./utils'); // 依次尝试加载:utils.js → utils.json → utils.node
- 目录作为模块:
javascript
require('./mylib'); // 尝试加载:
// 1. ./mylib/package.json 中的 "main" 字段指定的文件
// 2. 若不存在 package.json,则加载 ./mylib/index.js
// 3. 若以上都不存在,则报错
(3)npm 包
- 从当前目录的
node_modules
文件夹中查找。 - 逐级向上层目录的
node_modules
查找,直到根目录。
/project/src/index.js 中 require('lodash') 的查找路径:
/project/src/node_modules/lodash
/project/node_modules/lodash
/node_modules/lodash
模块缓存
Node.js 对模块实行缓存机制,同一模块仅在首次加载时执行一次,后续引用直接返回缓存结果。
javascript
// module.js
console.log('Module initialized');
module.exports = { value: 42 };
// main.js
const mod1 = require('./module');
const mod2 = require('./module');
console.log(mod1 === mod2); // true(两次引用指向同一对象)
注意:若需多次执行模块代码,需手动清除缓存(不推荐,可能导致意外行为):
javascript
delete require.cache[require.resolve('./module')];