CommonJS 模块

发布时间 2023-12-11 10:53:42作者: 心意12

在 Node.js 中,每个文件都被视为一个单独的模块。

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 模块加载器。

访问主模块

当文件直接从 Node.js 运行时,则 require.main 被设置为其 module。 这意味着可以通过测试 require.main === module 来确定文件是否被直接运行。

对于文件 foo.js,如果通过 node foo.js 运行,则为 true,如果通过 require('./foo') 运行,则为 false

当入口点不是 CommonJS 模块时,则 require.main 为 undefined,且主模块不可达。

包管理器的提示

为了使模块可用于 Node.js 交互式解释器,将 /usr/lib/node_modules 文件夹添加到 $NODE_PATH 环境变量可能会很有用。 由于使用 node_modules 文件夹的模块查找都是相对的,并且基于调用 require() 的文件的真实路径,因此包本身可以位于任何位置。

总结

要获取调用 require() 时将加载的确切文件名,则使用 require.resolve() 函数。

 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 是否是同一个文件。

核心模块

可以使用 node: 前缀来识别核心模块,在这种情况下它会绕过 require 缓存。 例如,require('node:http') 将始终返回内置的 HTTP 模块,即使有该名称的 require.cache 条目。

如果某些核心模块的标识符传给 require(),则总是优先加载它们。 例如,require('http') 将始终返回内置的 HTTP 模块,即使存在该名称的文件。 不使用 node: 前缀可以加载的核心模块列表暴露为 module.builtinModules

循环

当有循环 require() 调用时,模块在返回时可能尚未完成执行。a.js里require(b.js),b.js里require(a.js),这种情况是循环。

文件模块

如果找不到确切的文件名,Node.js 将尝试加载所需的文件名,并添加扩展名:.js.json,最后是 .node。 当加载具有不同扩展名的文件(例如 .cjs)时,则必须将其全名传给 require(),包括其文件扩展名(例如 require('./file.cjs')

.json 文件被解析为 JSON 文本文件,.node 文件被解释为加载了 process.dlopen() 的已编译插件模块。使用任何其他扩展名(或根本没有扩展名)的文件被解析为 JavaScript 文本文件。

以 '/' 为前缀的必需模块是文件的绝对路径。

以 './' 为前缀的必需模块与调用 require() 的文件相关。

如果没有前导 '/''./' 或 '../' 来指示文件,则该模块必须是核心模块或从 node_modules 文件夹加载。

如果给定路径不存在,则 require() 将抛出 MODULE_NOT_FOUND 错误。

目录作为模块

可以通过三种方式将文件夹作为参数传给 require()。

// 文件夹some-library中有package.json文件,并且内容如下所示
{ "name" : "some-library",
  "main" : "./lib/some-library.js" }

则 require('./some-library') 将尝试加载 ./some-library/lib/some-library.js

如果目录中没有package.json或者没有main或无法解析,则require('./some-library') 将尝试加载:

  • ./some-library/index.js
  • ./some-library/index.node

如果上面都找不到,则报错:Error: Cannot find module 'some-library'

从 node_modules 目录加载

如果传给 require() 的模块标识符不是核心模块,并且不以 '/''../' 或 './' 开头,则 Node.js 从当前模块的目录开始,并添加 /node_modules,并尝试从该位置加载模块。 Node.js 不会将 node_modules 附加到已经以 node_modules 结尾的路径。

如果在那里找不到它,则它移动到父目录,依此类推,直到到达文件系统的根目录。

通过在模块名称后包含路径后缀,可以要求与模块一起分发的特定文件或子模块。 例如,require('example-module/path/to/file') 将相对于 example-module 所在的位置解析 path/to/file。 后缀路径遵循相同的模块解析语义。

注意:require('example-module/path/to/file') 做一下测试,最后的file取的是node_modules中的example-module/path/to/file.js或者file.json或者file.node?如果都没有取到,会从example-module/path/to/node-module/file.js取么? 照理说一般情况下,file.js都会有的。

从全局目录加载

如果 NODE_PATH 环境变量设置为以冒号分隔的绝对路径列表,则 Node.js 将在这些路径中搜索模块(如果它们在其他地方找不到)。

模块封装器

在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:

(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});

通过这样做,Node.js 实现了以下几点:

  • 它将顶层变量(使用 varconst 或 let 定义)保持在模块而不是全局对象的范围内。
  • 它有助于提供一些实际特定于模块的全局变量,例如:
    • module 和 exports 对象,实现者可以用来从模块中导出值。
    • 便利变量 __filename 和 __dirname,包含模块的绝对文件名和目录路径。

模块作用域

__dirname

当前模块的目录名。 这与 __filename 的 path.dirname() 相同。

console.log(__dirname);
// 打印: /Users/mjr
console.log(path.dirname(__filename));
// 打印: /Users/mjr

__filename

当前模块的文件名。 这是当前模块文件的已解析符号链接的绝对路径。

console.log(__filename);
// 打印: /Users/mjr/example.js
console.log(__dirname);
// 打印: /Users/mjr

exports

对 module.exports 的引用,其输入更短。 有关何时使用 exports 和何时使用 module.exports 的详细信息,请参阅有关导出的快捷方式的章节。

module

对当前模块的引用,请参阅有关 module 对象的部分。 特别是,module.exports 用于定义模块通过 require() 导出和提供的内容。

require(id)

用于导入模块、JSON 和本地文件。

require.cache

模块在需要时缓存在此对象中。 通过从此对象中删除键值,下一次 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.main

Module 对象代表 Node.js 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined

在 entry.js 脚本中:

// 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])

  • request <string> 要解析的模块路径。
  • options <Object>
    • paths <string[]> 从中解析模块位置的路径。 如果存在,则使用这些路径而不是默认的解析路径,除了 GLOBAL_FOLDERS(例如 $HOME/.node_modules,其总是被包含在内)。 这些路径中的每一个都用作模块解析算法的起点,这意味着从此位置检查 node_modules 层级。
  • 返回: <string>

使用内部的 require() 工具查找模块的位置,但不加载模块,只返回解析的文件名。

如果找不到模块,则会抛出 MODULE_NOT_FOUND 错误。

require.resolve.paths(request)

如果 request 字符串引用核心模块,例如 http 或 fs,则返回包含在解析 request 或 null 期间搜索的路径的数组。

module 对象

在每个模块中,module 自由变量是对代表当前模块的对象的引用。 为方便起见,module.exports 也可通过 exports 模块全局访问

module.children

这个模块首次需要的对象。 require() 需要的对象

赋值给 module.exports 必须立即完成。 不能在任何回调中完成。 以下不起作用:

// x.js:
setTimeout(() => { module.exports = { a: 'hello' }; }, 0);
// y.js
const x = require('./x');
console.log(x.a);
导出的快捷方式

exports 变量在模块的文件级作用域内可用

请注意,与任何变量一样,如果将新值分配给 exports,则它就不再绑定到 module.exports

module.exports.hello = true; // 从模块的 require 中导出
exports = { hello: false };  // 未导出,仅在模块中可用

module.filename

模块的完全解析文件名。

module.id

模块的标识符。 通常这是完全解析的文件名。

module.isPreloading

  • 类型: <boolean> 如果模块在 Node.js 预加载阶段运行,则为 true

module.loaded

模块是否已完成加载,或正在加载。

module.path

模块的目录名称。 这通常与 module.id 的 path.dirname() 相同。

module.paths

模块的搜索路径。

module.require(id)

  • 返回: <any> 导出的模块内容

module.require() 方法提供了一种加载模块的方法,就像从原始模块调用 require() 一样。