Node.js module模块完整指南:加载机制、API与最佳实践
本文深入解析Node.js的module模块,涵盖模块加载系统、文件与目录解析规则、模块包装器原理及缓存机制。详细讲解module.exports与exports的区别、循环依赖处理、核心API如require.main和require.cache的使用,以及module对象属性。
本文详细介绍Node.js中module模块的具体用法与实例,旨在帮助开发者深入理解并掌握模块系统,供学习参考。
Node.js API详解之 module
Node.js内置了一套简洁高效的模块加载系统,负责管理代码的模块化组织。
在Node.js环境中,文件与模块存在严格的一一对应关系,即每个文件都被视为一个独立的模块单元。
以下是一个具体示例,假设存在一个名为 foo.js 的文件:
const circle = require('./circle.js');
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);
在该文件的第一行代码中,foo.js 通过 require 加载了位于同一目录下的 circle.js 模块。
circle.js 文件的具体内容如下所示:
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
circle.js 模块通过 exports 对象导出了 area() 和 circumference() 两个功能函数。
开发者可以通过在特殊的 exports 对象上挂载额外的属性,将函数或对象添加到模块的根层级。
模块内部定义的本地变量具有私有性,这是因为Node.js在执行模块代码前,会将其包裹在一个函数中(具体机制见模块包装器)。
在上述示例中,变量 PI 仅作为 circle.js 的私有变量存在。
module.exports 属性允许被赋予全新的值,例如一个函数或对象实例。
如下示例中,bar.js 引入了 square 模块,该模块导出一个构造函数:
const square = require('./square.js');
const mySquare = square(2);
console.log(`正方形的面积是 ${mySquare.area()}`);
square 模块的具体定义位于 square.js 文件中:
// 赋值给 `exports` 不会修改模块,必须使用 `module.exports`
module.exports = (width) => {
return {
area: () => width ** 2
};
};
整个模块系统由 Node.js 内部的 'module' 模块实现。
模块包装器
说明:
在执行模块代码之前,Node.js 会使用如下形式的函数包装器将其包裹:
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
通过这种包装机制,Node.js 实现了以下关键特性: 它确保了顶层变量(使用 var、const 或 let 定义)的作用域限制在模块内部,而非全局对象。 它提供了若干看似全局实则模块特定的变量,例如: 实现者可以利用 module 和 exports 对象来导出模块值。 包含模块绝对文件名和目录路径的快捷变量 __filename 和 __dirname 。
文件模块
说明:
若未通过确切文件名找到模块,Node.js 会依次尝试添加 .js、.json 或 .node 扩展名进行加载。 .js 文件会被解析为 JavaScript 文本文件, .json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 dlopen 加载的编译后插件模块。 以 ‘/' 为前缀的模块路径代表文件的绝对路径。例如,require(‘/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。 以 ‘./' 为前缀的模块路径表示相对于调用 require() 的文件的位置。也就是说,circle.js 必须与 foo.js 位于同一目录下,require(‘./circle') 才能成功找到它。 当模块标识符不以 ‘/'、'./' 或 ‘../' 开头时,该模块必须是核心模块或从 node_modules 目录加载。 如果指定的路径不存在,require() 将抛出一个带有 code 属性为 ‘MODULE_NOT_FOUND' 的 Error。
目录作为模块
说明:
可以将程序和库放置在单独的目录中,并提供一个单一的入口点指向该目录。 将目录路径作为参数传递给 require() 有三种处理方式。 第一种方式是在目录根目录下创建 package.json 文件,并指定一个 main 模块。
示例中,package.json 文件内容类似如下:
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
若该配置位于 ./some-library 目录中,则 require(‘./some-library') 会尝试加载 ./some-library/lib/some-library.js。 这就是 Node.js 处理 package.json 文件的标准方式。 注意:如果 package.json 中 “main” 字段指定的文件不存在,解析将失败,Node.js 会将该模块视为不存在,并抛出默认错误:
Error: Cannot find module 'some-library'
若目录中不存在 package.json 文件,Node.js 会尝试加载目录下的 index.js 或 index.node 文件。 例如,若上述示例中没有 package.json 文件,则 require(‘./some-library') 会尝试加载: ./some-library/index.js ./some-library/index.node
模块加载顺序
说明:
如果传递给 require() 的模块标识符不是核心模块,且不以 ‘/'、‘../' 或 ‘./' 开头, Node.js 会从当前模块的父目录开始,尝试从其 /node_modules 目录中加载模块。 Node.js 不会在已经以 node_modules 结尾的路径上附加 node_modules。 若仍未找到,则向上移动到上一级父目录,直至文件系统的根目录。 示例中,若在 ‘/home/ry/projects/foo.js' 文件中调用 require(‘bar.js'),则 Node.js 会按以下顺序查找: /home/ry/projects/node_modules/bar.js /home/ry/node_modules/bar.js /home/node_modules/bar.js /node_modules/bar.js 通过在模块名后包含路径后缀,可以请求特定的文件或分布式子模块。 例如,require(‘example-module/path/to/file') 会将 path/to/file 解析为相对于 example-module 的位置。 后缀路径同样遵循模块的解析语法。
从全局目录加载
说明:
如果 NODE_PATH 环境变量被设置为以冒号分隔的绝对路径列表,则当在其他位置找不到模块时,Node.js 会搜索这些路径。 注意:在 Windows 系统中,NODE_PATH 是以分号分隔的。 在当前的模块解析算法运行之前,NODE_PATH 最初是为了支持从不同路径加载模块而设计的。 虽然 NODE_PATH 仍然被支持,但现在已不太必要,因为 Node.js 生态系统已制定了一套存放依赖模块的约定。 有时当人们未意识到 NODE_PATH 必须被设置时,依赖 NODE_PATH 的部署会出现意外行为。 有时一个模块的依赖关系发生变化,导致在搜索 NODE_PATH 时加载了不同版本(甚至是不同的模块)。 此外,Node.js 还会搜索以下位置: 1: $HOME/.node_modules 2: $HOME/.node_libraries 3: $PREFIX/lib/node 其中 $HOME 是用户的主目录,$PREFIX 是 Node.js 配置中的 node_prefix。 这些主要是历史原因所致。 注意:强烈建议将所有依赖放置在本地的 node_modules 目录中。 这样加载速度更快,且更加可靠。
缓存
说明:
模块在首次加载后会被缓存。 这也意味着(与其他缓存机制类似),如果每次调用 require(‘foo') 都解析到同一文件,则返回相同的对象。 多次调用 require(foo) 不会导致模块代码被执行多次。 这是一个重要特性。 借助此特性,可以返回“部分完成”的对象,从而允许加载依赖的依赖,即使它们会导致循环依赖。 如果需要多次执行一个模块,可以导出一个函数,然后调用该函数。 注意: 模块是基于其解析后的文件名进行缓存的。 由于调用模块的位置不同,模块可能被解析为不同的文件名(例如从 node_modules 目录加载), 因此不能保证 require(‘foo') 总能返回完全相同的对象。 此外,在不区分大小写的文件系统或操作系统中,解析为不同文件名可能指向同一文件,但缓存仍会将它们视为不同的模块,并多次重新加载。 例如,require(‘./foo') 和 require(‘./FOO') 返回两个不同的对象,而不管 ./foo 和 ./FOO 是否是相同的文件。
循环
说明:
当出现循环调用 require() 时,一个模块可能在未完成执行时被返回。
例如以下情况: 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。 为了防止无限循环,会返回一个 a.js 的 exports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。 当 main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:
$ node main.js main 开始 a 开始 b 开始 在 b 中,a.done = false b 结束 在 a 中,b.done = true a 结束 在 main 中,a.done=true,b.done=true
__dirname
说明:
当前模块所在的文件夹名称。其值等同于 __filename 的 path.dirname() 返回值。
demo:
console.log(__dirname);
// /Users/xiaoqiang/Documents/work/demo/NodeApi
__filename
说明:
当前模块的文件名称——解析后的绝对路径。 在主程序中,这不一定与命令行中使用的名称一致。
demo:
console.log(__filename);
// /Users/xiaoqiang/Documents/work/demo/NodeApi/app.js
module.exports
说明:
module.exports 对象由模块系统创建。 有时这是不可接受的;许多人希望他们的模块成为某个类的实例。 为了实现这一点,需要将期望导出的对象赋值给 module.exports。 注意,将期望的对象赋值给 exports 会简单地重新绑定本地 exports 变量,这可能并非期望的结果。 注意,对 module.exports 的赋值必须立即完成。 不能在任何回调中完成。
demo:
// a.js
const EventEmitter = require('events');
module.exports = new EventEmitter();
// app.js
const a = require('./a');
a.on('ready', () => {
console.log('模块 a 已准备好');
});
exports
说明:
这是一个对于 module.exports 的更简短的引用形式。 exports 变量在模块的文件级别作用域内有效,它在模块被执行前被赋予 module.exports 的值。 它提供了一个快捷方式,以便 module.exports.f = … 可以被更简洁地写成 exports.f = …。 注意,就像任何变量,如果一个新的值被赋值给 exports,它就不再绑定到 module.exports:
demo:
// a.js
const EventEmitter = require('events');
exports.events = new EventEmitter();
// app.js
const {events} = require('./a');
events.on('ready', () => {
console.log('模块 a 已准备好');
});
require()
说明:
使用该方法引入模块。
demo:
const {events} = require('./a');
events.on('ready', () => {
console.log('模块 a 已准备好');
});
require.main
说明:
当 Node.js 直接运行一个文件时,require.main 会被设为它的 module。 这意味着可以通过 require.main === module 来判断一个文件是否被直接运行: 对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require(‘./foo') 运行则为 false。 因为 module 提供了一个 filename 属性(通常等同于 __filename), 所以可以通过检查 require.main.filename 来获取当前应用程序的入口点。
demo:
const events = require('./a');
console.log(require.main);
// Module {
// id: '.',
// exports: {},
// parent: null,
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/app.js',
// loaded: false,
// children:
// [ Module {
// id: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// exports: [Object],
// parent: [Circular],
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// loaded: true,
// children: [],
// paths: [Array] } ],
// paths:
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi/node_modules',
// '/Users/xiaoqiang/Documents/work/demo/node_modules',
// '/Users/xiaoqiang/Documents/work/node_modules',
// '/Users/xiaoqiang/Documents/node_modules',
// '/Users/xiaoqiang/node_modules',
// '/Users/node_modules',
// '/node_modules' ]
// }
require.cache
说明:
被引入的模块将被缓存在这个对象中。 从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。 注意不能删除 native addons(原生插件),因为它们的重载将会导致错误。
demo:
const events = require('./a');
console.log(require.cache);
// { '/Users/xiaoqiang/Documents/work/demo/NodeApi/app.js':
// Module {
// id: '.',
// exports: {},
// parent: null,
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/app.js',
// loaded: false,
// children: [ [Module] ],
// paths:
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi/node_modules',
// '/Users/xiaoqiang/Documents/work/demo/node_modules',
// '/Users/xiaoqiang/Documents/work/node_modules',
// '/Users/xiaoqiang/Documents/node_modules',
// '/Users/xiaoqiang/node_modules',
// '/Users/node_modules',
// '/node_modules' ] },
// '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js':
// Module {
// id: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// exports: { events: [EventEmitter] },
// parent:
// Module {
// id: '.',
// exports: {},
// parent: null,
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/app.js',
// loaded: false,
// children: [Array],
// paths: [Array] },
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// loaded: true,
// children: [],
// paths:
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi/node_modules',
// '/Users/xiaoqiang/Documents/work/demo/node_modules',
// '/Users/xiaoqiang/Documents/work/node_modules',
// '/Users/xiaoqiang/Documents/node_modules',
// '/Users/xiaoqiang/node_modules',
// '/Users/node_modules',
// '/node_modules' ]
// }
// }
require.extensions (已废弃)
说明:
指示 require 如何处理特定的文件扩展名。 以前这被用来将非 JavaScript 模块按需编译后加载到 Node.js 中。 然而,在实践中,有更多更好的解决方案,比如用其它 Node.js 程序加载模块, 或者提前将它们编译为 JavaScript 模块。 由于模块系统已锁定,这个特性可能永远不会消失,但是鉴于其复杂性和可能导致的小问题, 最好不要碰它。 例如:把 .sjs 文件当做 .js 文件处理:
demo:
require.extensions['.sjs'] = require.extensions['.js'];
require.resolve(request[, options])
说明:
使用内部的 require() 机制查询模块的位置, 此操作只返回解析后的文件名,不会加载该模块。 request:需要解析的模块路径。 options.paths:解析模块的起点路径数组。此参数存在时,将使用这些路径而非默认解析路径。
demo:
const events = require('./a');
console.log( require.resolve('./a') );
// /Users/xiaoqiang/Documents/work/demo/NodeApi/a.js
require.resolve.paths(request)
说明:
返回一个数组,其中包含解析 request 过程中被查询的路径。 request:被查询解析路径的模块的路径。
demo:
const events = require('./a');
console.log( require.resolve.paths('./a') );
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi' ]
module
说明:
返回对当前模块的引用,是一个module对象。
demo:
const events = require('./a');
console.log( module );
// Module {
// id: '.',
// exports: {},
// parent: null,
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/app.js',
// loaded: false,
// children:
// [ Module {
// id: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// exports: [Object],
// parent: [Circular],
// filename: '/Users/xiaoqiang/Documents/work/demo/NodeApi/a.js',
// loaded: true,
// children: [],
// paths: [Array] } ],
// paths:
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi/node_modules',
// '/Users/xiaoqiang/Documents/work/demo/node_modules',
// '/Users/xiaoqiang/Documents/work/node_modules',
// '/Users/xiaoqiang/Documents/node_modules',
// '/Users/xiaoqiang/node_modules',
// '/Users/node_modules',
// '/node_modules' ]
// }
module 对象
说明:
在每个模块中,module 的自由变量是一个指向表示当前模块的对象的引用。 为了方便,module.exports 也可以通过全局模块的 exports 对象访问。 module 实际上不是全局的,而是每个模块本地的。
module.id
说明:
返回模块的标识符。 通常是完全解析后的文件名。
demo:
const events = require('events');
console.log( module.id );
// .
module.parent
说明:
最先引用该模块的模块。
demo:
const events= require('events');
console.log( module.parent );
// null
module.children
说明:
返回被该模块引用的模块对象。
demo:
const events = require('events');
console.log( module.children );
// []
module.filename
说明:
返回模块的完全解析后的文件名。
demo:
const events = require('events');
console.log( module.filename );
// /Users/xiaoqiang/Documents/work/demo/NodeApi/app.js
module.loaded
说明:
返回模块是否已经加载完成,或正在加载中。
demo:
const events = require('events');
console.log( module.loaded );
// false
module.paths
说明:
返回模块的搜索路径。
demo:
const events = require('events');
console.log( module.paths );
// [ '/Users/xiaoqiang/Documents/work/demo/NodeApi/node_modules',
// '/Users/xiaoqiang/Documents/work/demo/node_modules',
// '/Users/xiaoqiang/Documents/work/node_modules',
// '/Users/xiaoqiang/Documents/node_modules',
// '/Users/xiaoqiang/node_modules',
// '/Users/node_modules',
// '/node_modules' ]
希望本文所述对大家node.js程序设计有所帮助。
NestJS 是什么?与 Express、Koa 的核心区别及选型建议
2026-11-05 13:46:41
生产环境升级 Node.js 的注意事项与兼容性测试策略
2026-11-05 09:19:24
nvm 常用命令详解:怎么平滑切换和升级 Node.js 版本
2026-11-04 09:02:34
JavaScript和JSP的根本区别:运行环境与执行机制解析
2026-11-03 23:30:18
使用nvm卸载指定版本Node.js并切换默认环境
2026-11-03 23:09:26
