CommonJS 模块是为 Node.js 打包 JavaScript 代码的原始方式。
Node.js 还支持浏览器和其他 JavaScript 运行时使用的
ECMAScript 模块
标准。
在 Node.js 中,每个文件都被视为一个单独的模块。
例如,假设一个名为
foo.js
的文件:
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
在第一行,
foo.js
加载了与
foo.js
位于同一目录中的模块
circle.js
。
以下是
circle.js
的内容:
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
模块
circle.js
已导出函数
area()
和
circumference()
。
通过在特殊的
exports
对象上指定额外的属性,将函数和对象添加到模块的根部。
模块的本地变量将是私有的,因为模块被 Node.js 封装在函数中(参见
模块封装器
)。
在此示例中,变量
PI
是
circle.js
私有的。
可以为
module.exports
属性分配新的值(例如函数或对象)。
下面,
bar.js
使用了导出 Square 类的
square
模块:
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
square
模块在
square.js
中定义:
module.exports = class Square {
constructor(width) {
this.width = width;
area() {
return this.width ** 2;
CommonJS 模块系统在 module
核心模块中实现。
Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。
默认情况下,Node.js 会将以下内容视为 CommonJS 模块:
扩展名为 .cjs
的文件;
当最近的父 package.json
文件包含值为 "commonjs"
的顶层字段 "type"
时,则扩展名为 .js
的文件。
当最近的父 package.json
文件不包含顶层字段 "type"
时,则扩展名为 .js
的文件。
包作者应该包括 "type"
字段,即使在所有源都是 CommonJS 的包中也是如此。
明确包的 type
将使构建工具和加载器更容易确定包中的文件应该如何解释。
扩展名不是 .mjs
、.cjs
、.json
、.node
、或 .js
的文件(当最近的父 package.json
文件包含值为 "module"
的顶层字段 "type"
时,这些文件只有在它们是 require
的,而不是用作程序的命令行入口点)。
参阅确定模块系统了解更多详细信息。
调用 require()
始终使用 CommonJS 模块加载器。
调用 import()
始终使用 ECMAScript 模块加载器。
.mjs 扩展名#
由于 require()
的同步特性,无法使用它来加载 ECMAScript 模块文件。
尝试这样做会抛出 ERR_REQUIRE_ESM
错误。
改用 import()
。
.mjs
扩展名是为无法通过 require()
加载的 ECMAScript 模块保留的。
有关哪些文件被解析为 ECMAScript 模块的更多信息,请参阅确定模块系统章节。
要获取调用 require()
时将加载的确切文件名,则使用 require.resolve()
函数。
综上所述,这里是 require()
的伪代码高级算法:
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. If X begins with '#'
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. If "main" is a falsy value, GOTO 2.
c. let M = X + (json main field)
d. LOAD_AS_FILE(M)
e. LOAD_INDEX(M)
f. LOAD_INDEX(X) DEPRECATED
g. THROW "not found"
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIR + DIRS
d. let I = I - 1
5. return DIRS + GLOBAL_FOLDERS
LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
["node", "require"]) defined in the ESM resolver.
5. RESOLVE_ESM_MATCH(MATCH).
LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", ["node", "require"]) defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "exports" is null or undefined, return.
4. If the SCOPE/package.json "name" is not the first segment of X, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
"." + X.slice("name".length), `package.json` "exports", ["node", "require"])
defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
RESOLVE_ESM_MATCH(MATCH)
1. let { RESOLVED, EXACT } = MATCH
2. let RESOLVED_PATH = fileURLToPath(RESOLVED)
3. If EXACT is true,
a. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
format. STOP
4. Otherwise, if EXACT is false,
a. LOAD_AS_FILE(RESOLVED_PATH)
b. LOAD_AS_DIRECTORY(RESOLVED_PATH)
5. THROW "not found"
模块在第一次加载后被缓存。
这意味着(类似其他缓存)每次调用 require('foo')
都会返回完全相同的对象(如果解析为相同的文件)。
如果 require.cache
没有被修改,则多次调用 require('foo')
不会导致模块代码被多次执行。
这是重要的特征。
有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。
要让模块多次执行代码,则导出函数,然后调用该函数。
模块缓存的注意事项#
模块根据其解析的文件名进行缓存。
由于模块可能会根据调用模块的位置(从 node_modules
文件夹加载)解析为不同的文件名,因此如果 require('foo')
解析为不同的文件,则不能保证 require('foo')
将始终返回完全相同的对象。
此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并将多次重新加载文件。
例如,require('./foo')
和 require('./FOO')
返回两个不同的对象,而不管 ./foo
和 ./FOO
是否是同一个文件。
版本变更
v16.0.0
为 require(...)
添加了 node:
导入支持。
Node.js 有些模块编译成二进制文件。
这些模块在本文档的其他地方有更详细的描述。
核心模块在 Node.js 源代码中定义,位于 lib/
文件夹中。
可以使用 node:
前缀来识别核心模块,在这种情况下它会绕过 require
缓存。
例如,require('node:http')
将始终返回内置的 HTTP 模块,即使有该名称的 require.cache
条目。
如果某些核心模块的标识符传给 require()
,则总是优先加载它们。
例如,require('http')
将始终返回内置的 HTTP 模块,即使存在该名称的文件。
不使用 node:
前缀可以加载的核心模块列表暴露为 module.builtinModules
。
当有循环 require()
调用时,模块在返回时可能尚未完成执行。
考虑这种情况:
a.js
:
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in 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
导出对象的未完成副本返回给 b.js
模块。
然后 b.js
完成加载,并将其 exports
对象提供给 a.js
模块。
到 main.js
加载这两个模块时,它们都已完成。
因此,该程序的输出将是:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
需要仔细规划以允许循环模块依赖项在应用程序中正常工作。
目录作为模块#
可以通过三种方式将文件夹作为参数传给 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
。
如果目录中不存在 package.json
文件,或者 "main"
条目丢失或无法解析,则 Node.js 将尝试从该目录中加载 index.js
或 index.node
文件。
例如,如果前面的示例中没有 package.json
文件,则 require('./some-library')
将尝试加载:
./some-library/index.js
./some-library/index.node
如果这些尝试失败,Node.js 将报告整个模块丢失,并显示默认错误:
Error: Cannot find module 'some-library'
在上述所有三种情况下,import('./some-library')
调用都将导致 ERR_UNSUPPORTED_DIR_IMPORT
错误。
使用包子路径导出或子路径导入可以提供与文件夹作为模块相同的包含组织优势,并且适用于 require
和 import
。
从 node_modules 目录加载#
如果传给 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')
将相对于 example-module
所在的位置解析 path/to/file
。
后缀路径遵循相同的模块解析语义。
新增于: v0.1.27
<string>
当前模块的目录名。
这与 __filename
的 path.dirname()
相同。
示例:从 /Users/mjr
运行 node example.js
console.log(__dirname);
console.log(path.dirname(__filename));
__filename
#
新增于: v0.0.1
<string>
当前模块的文件名。
这是当前模块文件的已解析符号链接的绝对路径。
对于主程序,这不一定与命令行中使用的文件名相同。
当前模块的目录名见 __dirname
。
从 /Users/mjr
运行 node example.js
console.log(__filename);
console.log(__dirname);
给定两个模块:a
和 b
,其中 b
是 a
的依赖项,且目录结构为:
/Users/mjr/app/a.js
/Users/mjr/app/node_modules/b/b.js
在 b.js
中对 __filename
的引用将返回 /Users/mjr/app/node_modules/b/b.js
,而在 a.js
中对 __filename
的引用将返回 /Users/mjr/app/a.js
。
exports
#
新增于: v0.1.12
<Object>
对 module.exports
的引用,其输入更短。
有关何时使用 exports
和何时使用 module.exports
的详细信息,请参阅有关导出的快捷方式的章节。
module
#
新增于: v0.1.16
<module>
对当前模块的引用,请参阅有关 module
对象的部分。
特别是,module.exports
用于定义模块通过 require()
导出和提供的内容。
require(id)
#
新增于: v0.1.13
id
<string> 模块名称或路径
返回: <any> 导出的模块内容
用于导入模块、JSON
和本地文件。
模块可以从 node_modules
导入。
可以使用相对路径(例如 ./
、./foo
、./bar/baz
、../foo
)导入本地模块和 JSON 文件,该路径将根据 __dirname
(如果有定义)命名的目录或当前工作目录进行解析。
POSIX 风格的相对路径以独立于操作系统的方式解析,这意味着上面的示例将在 Windows 上以与在 Unix 系统上相同的方式工作。
const myLocalModule = require('./path/myLocalModule');
const jsonData = require('./path/filename.json');
const crypto = require('node:crypto');
require.cache
#
新增于: v0.3.0
<Object>
模块在需要时缓存在此对象中。
通过从此对象中删除键值,下一次 require
将重新加载模块。
这不适用于原生插件,因为重新加载会导致错误。
添加或替换条目也是可能的。
在内置模块之前检查此缓存,如果将与内置模块匹配的名称添加到缓存中,则只有 node:
前缀的 require 调用将接收内置模块。
小心使用!
const assert = require('node:assert');
const realFs = require('node:fs');
const fakeFs = {};
require.cache.fs = { exports: fakeFs };
assert.strictEqual(require('node:fs'), fakeFs);
assert.strictEqual(require('node:fs'), realFs);
require.extensions
#
新增于: v0.3.0弃用于: v0.10.6
<Object>
指导 require
如何处理某些文件扩展名。
将扩展名为 .sjs
的文件处理为 .js
:
require.extensions['.sjs'] = require.extensions['.js'];
已弃用。 过去,此列表用于通过按需编译将非 JavaScript 模块加载到 Node.js 中。
但是,在实践中,有很多更好的方法可以做到这一点,例如通过其他一些 Node.js 程序加载模块,或者提前将它们编译为 JavaScript。
避免使用 require.extensions
。
使用可能会导致细微的错误,并且每个注册的扩展程序解决扩展程序的速度都会变慢。
require.main
#
新增于: v0.1.17
<module> | <undefined>
Module
对象代表 Node.js 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined
。
请参阅“访问主模块”。
在 entry.js
脚本中:
console.log(require.main);
node entry.js
Module {
id: '.',
path: '/absolute/path/to',
exports: {},
filename: '/absolute/path/to/entry.js',
loaded: false,
children: [],
paths:
[ '/absolute/path/to/node_modules',
'/absolute/path/node_modules',
'/absolute/node_modules',
'/node_modules' ] }
require.resolve(request[, options])
#
版本变更
v8.9.0
现在支持 paths
选项。
v0.3.0
新增于: v0.3.0
paths
<string[]> 从中解析模块位置的路径。
如果存在,则使用这些路径而不是默认的解析路径,除了 GLOBAL_FOLDERS(例如 $HOME/.node_modules
,其总是被包含在内)。
这些路径中的每一个都用作模块解析算法的起点,这意味着从此位置检查 node_modules
层级。
返回: <string>
使用内部的 require()
工具查找模块的位置,但不加载模块,只返回解析的文件名。
如果找不到模块,则会抛出 MODULE_NOT_FOUND
错误。
require.resolve.paths(request)
#
新增于: v8.9.0
request
<string> 正在检索其查找路径的模块路径。
返回: <string[]> | <null>
如果 request
字符串引用核心模块,例如 http
或 fs
,则返回包含在解析 request
或 null
期间搜索的路径的数组。