CommonJS 是由 JavaScript 社区于 2oo9 年提出的包含模块、文件、IO、控制台在内的一系列标准。Node.js 的实现中采用了 CommonJS 标准的一部分,并在其基础上进行了一些调整。我们所说的 CommonJS 模块和 Node.js 中的实现并不完全一样,现在一般谈到 CommonJS 其实是 Node.js 中的版本,而非它的原始定义。
CommonJS 最初只为服务端而设计,直到有了 Browserify ——一个运行在 Node.js 环境下的模块打包工具,它可以将 CommonJS 模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循 CommonJS 标准来编写了。
不仅如此,借 Node.js 的包管理器,npm 开发者还可以获取他人的代码库,或者把自己的代码发布上去供他人使用。这种可共享的传播方式使 CommonJS 在前端开发领域逐渐流行起来。
模块
通过 script 标签插入的 JS 文件的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境:而通过 CommonJS 引入的 JS 文件会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看下面的例子:
//calculator.js
var name = 'calculator.js';
//index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js
这里有两个文件,在 index.js 中我们通过 CommonJS 的 require 函数加载 calculator.js。运行之后控制台结果是 “index.js”,说明 calculator.js 中的变量声明并不会影响 index.js,可见每个模块是拥有各自的作用域的。
导出
导出是一个模块向外暴露自身的唯一方式。在 CommonJS 中,通过 module.exports
可以导出模块中的内容,如:
module.exports = {name: 'calculater'add: function(a, b){return a + b;}
}
CommonJS 模块内部会用一个 module
对象存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象:
var module = {//...};
// 模块自身逻辑
module.exports = {//...};
module.exports
用来指定该模块要对外暴露哪些内容,为了书写方便,CommonJS 也支持另种简化的导出方式——直接使用 exports
:
exports.name = 'calculater';
exports.add = function(a,b){return a + b;
}
其内在机制是将 exports
指向 module.exports
,而 module.exports 在初始化时是一个空对象。我们可以简单地理解为,CommonJS 在每个模块的首部默认添加了以下代码:
var module = {exports: {}
};
var exports = module.exports;
但不能直接给 exports 赋值,否则会导致其失效。还有禁止将 module.exports 和 exports 混用。
导入
在 CommonJS 中使用 require
语法进行模块导入。如:
// calculator.js
module.exports = { add: function(a, b){return a + b;}
}
// index.js
const calculator = require('./calculator.js');
const sum = calculator.add(2, 3);
console.log(sum); // 5
当我们使用 require
导入一个模块时会有两种情况:
- 该模块未曾被加载过。这时会首先执行该模块,然后获取到该模块最终导出的内容。
- 该模块已经被加我过。这时该模块的代码不会再次执行,而是直接获取该模块上一次导出的内容。
我们前面提到,模块会有一个 module
为象用来存放其信息,这个对象中有一个属性 loaded
用于记录该模块是否被加载过。loaded
的值默认为 false
,在模块第一次被加载和执行过后会置为true
,后面再次加载时检查到 module.loaded
为 true
,则不会再次执行模块代码。
有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用 require
即可。
require('./task.js');
模块封装器
在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:
(function(exports, require, module, __filename, __dirname) {// Module code actually lives in here
});
通过这样做,Node.js 实现了以下几点:
- 它将顶层变量(使用
var
、const
或let
定义)保持在模块而不是全局对象的范围内。 - 它有助于提供一些实际特定于模块的全局变量,例如:
module
和exports
对象,实现者可以用来从模块中导出值。- 便利变量
__filename
和__dirname
,包含模块的绝对文件名和目录路径。
模块作用域
__dirname
当前模块的目录名。这与 __filename
的 path.dirname()
相同。
__filename
当前模块的文件名。这是当前模块文件的已解析符号链接的绝对路径。对于主程序,这不一定与命令行中使用的文件名相同。
exports
对 module.exports
的引用,其输入更短。
直接给 module.exports
或 exports
赋值都会断开引用关系。
直接赋值会导致以下后果:
moudle
当前模块的引用,请参阅关于 module
对象的部分。特别是,module.exports
用于定义模块导出的内容,并通过 require()
使其可用。
require(id: string)
id
—— 模块名称或路径返回值
—— 模块导出的内容
用于导入模块、JSON
和本地文件。模块可以从 node_modules
导入。可以使用相对路径(例如 ./
、./foo
、./bar/baz
、../foo
)导入本地模块和 JSON 文件,该路径将根据 __dirname
(如果有定义)命名的目录或当前工作目录进行解析。
require.cache
- 第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的
module.exports
属性(不会再次执行该模块)。 - 如果需要多次执行模块中的代码,一般可以让模块暴露行为(函数)。
- 模块的缓存可以通过
require.cache
拿到,也可以从此对象中删除键值,下一次require
将重新加载模块。
require.main
Module
对象代表 Node.js 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined
。参见访问主模块。
require.resolve(request[, options])
request
—— 要解析的模块路径。options
paths
从中解析模块位置的路径。如果存在,将使用这些路径而不是默认解析路径,但GLOBAL_FOLDERS
和$HOME/.node_modules
除外,它们始终包含在内。这些路径中的每一个都用作模块解析算法的起点,这意味着从此位置检查node_modules
层级。
require.resolve.paths(request)
request
—— 正在检索其查找路径的模块路径。- 返回一个数组,其中包含在解析请求期间搜索的路径,如果请求字符串引用核心模块,例如
http
或fs
,则返回null
。
moudle 对象
在每个模块中,module
自由变量是对代表当前模块的对象的引用。module
实际上不是全局的,而是每个模块本地的。
module.children
此模块首次需要的模块对象。
module.exports
从模块中导出任何你想导出的东西。
赋值给 module.exports
必须立即完成。不能在任何回调中完成。以下不起作用:
exports
参考 模块作用域->exports
module.filename
模块的完全解析文件名。
module.id
模块的标识符。通常这是完全解析的文件名。
module.isPreloading
如果模块在 Node.js 预加载阶段运行,则为 true
。
module.loaded
模块是否已完成加载,或正在加载。
module.parent
第一个需要此模块的模块,如果当前模块是当前进程的入口点,则为 null
;如果该模块是由非 CommonJS 模块加载的模块(例如:REPL 或 import
),则为 undefined
。
module.path
模块的目录名称。这通常与 module.id
的 path.dirname()
相同。
module.paths
模块的搜索路径。
module.require(id)
id
—— 模块名称或路径返回值
—— 模块导出的内容
module.require()
方法提供了一种加载模块的方法,就像从原始模块调用 require()
一样。为了做到这一点,有必要获取对模块对象的引用。由于 require()
返回 module.exports
,并且该模块通常仅在特定模块的代码中可用,因此必须显式导出才能使用。
基本上,用 module.requrie
是极少见的,借用它家的 module
更为少见。
module.builtinModules
Node.js 提供的所有模块的名称列表。可用于验证模块是否由第三方维护。
注意:列表不包含像 node:test
这样的仅前缀模块。
此上下文中的 module
与模块封装器提供的对象不同。要访问它,需要 Module
模块:
// import { builtinModules } from 'node:module' // es模块
const builtin = require('node:module').builtinModules;
module.createRequire(filename)
filename
—— 用于构造 require 函数的文件名。必须是文件网址对象、文件网址字符串、或绝对路径字符串。- 返回 ——
require
函数。
module.isBuiltin(moduleName)
moduleName
—— 模块名称。- 返回:—— 如果模块是内置的,则返回
true
,否则返回false
。
module.register(specifier[, parentURL] [, options])
specifier: <string>|<URL>
—— 需要注册的定制钩子;这应该与传递给import()
的字符串相同,但如果它是相对的,则它是相对于parentURL
解析的。parentURL: <string>|<URL>
—— 如果你想要相对于基本 URL(例如import.meta.url
)解析specifier
,你可以在此处传递该 URL。默认值:'data:'
options: <Object>
parentURL: <string>|<URL>
—— 如果你想要相对于基本 URL(例如import.meta.url
)解析specifier
,你可以在此处传递该 URL。如果parentURL
作为第二个参数提供,则忽略此属性。默认值:'data:'
data: <any>
—— 传递到initialize
钩子的任何任意的、可克隆的 JavaScript 值。transferList: <Object[]>
—— 可转换对象 要传递到initialize
钩子中。
注册一个导出钩子的模块,用于自定义 Node.js 模块解析和加载行为。参见定制钩子。
module.syncBuiltinESMExports()
module.syncBuiltinESMExports()
方法,用于更新所有内置 ES 模块的实时绑定,以匹配 CommonJS 模块的exports
属性。这个方法的主要目的是为了协调 CommonJS 和 ES6 模块之间的交互,确保在使用混合模块系统时,数据的同步和一致性。
此外,这个方法还涉及到对内置模块的修改和同步。虽然内置模块在运行时可以被修改,但不能新增或删除。通过module.syncBuiltinESMExports()
,可以确保这些修改被正确地同步到整个模块系统中,保持数据的一致性和完整性。
确定模块系统
当传递给 node
作为初始输入时,或者当被 import
语句或 import()
表达式引用时,Node.js 会将以下内容视为ES 模块:
- 扩展名为
.mjs
的文件。 - 当最近的父
package.json
文件包含值为"module"
的顶层"type"
字段时,扩展名为.js
的文件。 - 字符串作为参数传入
--eval
,或通过STDIN
管道传输到node
,带有标志--input-type=module
。 - 使用
--experimental-detect-module
时,包含语法的代码仅成功解析为ES 模块,例如import
或export
语句或import.meta
,没有明确标记应如何解释它。显式标记是.mjs
或.cjs
扩展、带有"module"
或"commonjs"
值的package.json
"type"
字段,或者--input-type
或--experimental-default-type
标志。CommonJS 或 ES 模块都支持动态import()
表达式,并且不会导致文件被视为 ES 模块。
当传递给 node
作为初始输入时,或者当被 import
语句或 import()
表达式引用时,Node.js 会将以下内容视为CommonJS:
- 扩展名为
.cjs
的文件。 - 当最近的父
package.json
文件包含值为"commonjs"
的顶层字段"type"
时,则扩展名为.js
的文件。 - 字符串作为参数传入
--eval
或--print
,或通过STDIN
管道传输到node
,带有标志--input-type=commonjs
。
除了这些明确的情况之外,还有其他情况,Node.js 根据 --experimental-default-type
标志的值默认使用一个模块系统或另一个模块系统:
- 如果同一文件夹或任何父文件夹中不存在
package.json
文件,则以.js
结尾或没有扩展名的文件。 - 如果最近的父
package.json
字段缺少"type"
字段,则以.js
结尾或没有扩展名的文件;除非该文件夹位于node_modules
文件夹内。(当package.json
文件缺少"type"
字段时,无论--experimental-default-type
如何,为了向后兼容,node_modules
下的包范围始终被视为 CommonJS。) - 当未指定
--input-type
时,字符串作为参数传递给--eval
或通过STDIN
通过管道传递给node
。
该标志当前默认为 "commonjs"
,但将来可能会更改为默认为 "module"
。因此,最好尽可能明确;特别是,包作者应始终在其 package.json
文件中包含 "type"
字段,即使在所有源都是 CommonJS 的包中也是如此。如果 Node.js 的默认类型发生变化,显式说明包的 type
将使包面向未来,它还将使构建工具和加载器更容易确定应如何解释包中的文件。
启用
Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。
默认情况下,Node.js 会将以下内容视为 CommonJS 模块:
- 扩展名为
.cjs
的文件; - 当最近的父
package.json
文件包含值为"commonjs"
的顶层字段"type"
时,则扩展名为.js
的文件。 - 当最近的父
package.json
文件不包含顶层字段"type"
或任何父文件夹中都没有package.json
时,具有.js
扩展名或不带扩展名的文件;除非该文件包含错误的语法,除非它被评估为 ES 模块。包作者应该包括"type"
字段,即使在所有源都是 CommonJS 的包中也是如此。明确包的type
将使构建工具和加载器更容易确定包中的文件应该如何解释。 - 扩展名不是
.mjs
、.cjs
、.json
、.node
或.js
的文件(当最近的父package.json
文件包含值为"module"
的顶层字段"type"
时,这些文件将被识别为 CommonJS 模块只有当它们是通过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
,且主模块不可达。