- assert断言
- async_hooks异步钩子
- async_hooks/context异步上下文
- buffer缓冲区
- C++插件
- C/C++插件(使用Node-API)
- C++嵌入器
- child_process子进程
- cluster集群
- CLI命令行
- console控制台
- Corepack核心包
- crypto加密
- crypto/webcrypto网络加密
- debugger调试器
- deprecation弃用
- dgram数据报
- diagnostics_channel诊断通道
- dns域名服务器
- domain域
- Error错误
- events事件触发器
- fs文件系统
- global全局变量
- http超文本传输协议
- http2超文本传输协议2.0
- https安全超文本传输协议
- inspector检查器
- Intl国际化
- module模块
- module/cjsCommonJS模块
- module/esmECMAScript模块
- module/package包模块
- net网络
- os操作系统
- path路径
- perf_hooks性能钩子
- permission权限
- policy安全策略
- process进程
- punycode域名代码
- querystring查询字符串
- readline逐行读取
- repl交互式解释器
- report诊断报告
- stream流
- stream/web网络流
- string_decoder字符串解码器
- test测试
- timers定时器
- tls安全传输层
- trace_events跟踪事件
- tty终端
- url网址
- util实用工具
- v8引擎
- vm虚拟机
- wasi网络汇编系统接口
- worker_threads工作线程
- zlib压缩
Node.js v16.19.1 文档
- Node.js 16.19.1
- ► 目录
-
►
索引
- assert 断言
- async_hooks 异步钩子
- async_hooks/context 异步上下文
- buffer 缓冲区
- C++插件
- C/C++插件(使用Node-API)
- C++嵌入器
- child_process 子进程
- cluster 集群
- CLI 命令行
- console 控制台
- Corepack 核心包
- crypto 加密
- crypto/webcrypto 网络加密
- debugger 调试器
- deprecation 弃用
- dgram 数据报
- diagnostics_channel 诊断通道
- dns 域名服务器
- domain 域
- Error 错误
- events 事件触发器
- fs 文件系统
- global 全局变量
- http 超文本传输协议
- http2 超文本传输协议2.0
- https 安全超文本传输协议
- inspector 检查器
- Intl 国际化
- module 模块
- module/cjs CommonJS模块
- module/esm ECMAScript模块
- module/package 包模块
- net 网络
- os 操作系统
- path 路径
- perf_hooks 性能钩子
- permission 权限
- policy 安全策略
- process 进程
- punycode 域名代码
- querystring 查询字符串
- readline 逐行读取
- repl 交互式解释器
- report 诊断报告
- stream 流
- stream/web 网络流
- string_decoder 字符串解码器
- test 测试
- timers 定时器
- tls 安全传输层
- trace_events 跟踪事件
- tty 终端
- url 网址
- util 实用工具
- v8 引擎
- vm 虚拟机
- wasi 网络汇编系统接口
- worker_threads 工作线程
- zlib 压缩
- ► 其他版本
- 文档搜索
package 包模块#
介绍#
包是由 package.json
文件描述的文件夹树。
包由包含 package.json
文件的文件夹和所有子文件夹组成,直到包含另一个 package.json
文件的下一个文件夹或名为 node_modules
的文件夹。
此页面为编写 package.json
文件的包作者提供指导,以及 Node.js 定义的 package.json
字段的参考。
确定模块系统#
当作为初始输入传入、或者当被 import
语句或 import()
表达式引用时,Node.js 会将以下视为ES 模块:
-
扩展名为
.mjs
的文件。 -
当最近的父
package.json
文件包含值为"module"
的顶层"type"
字段时,扩展名为.js
的文件。 -
字符串作为参数传入
--eval
,或通过STDIN
管道传输到node
,带有标志--input-type=module
。
Node.js 会将所有其他形式的输入视为 CommonJS,例如 .js
文件,其中最近的父 package.json
文件不包含顶层 "type"
字段,或者没有标志 --input-type
的字符串输入。
此行为是为了保持向后兼容性。
但是,现在 Node.js 同时支持 CommonJS 和 ES 模块,最好尽可能明确。
当作为初始输入传给 node
、或者当被 import
语句或 import()
表达式或 require()
表达式引用时,Node.js 会将以下视为 CommonJS:
-
扩展名为
.cjs
的文件。 -
当最近的父
package.json
文件包含值为"commonjs"
的顶层字段"type"
时,则扩展名为.js
的文件。 -
字符串作为参数传入
--eval
或--print
,或通过STDIN
管道传输到node
,带有标志--input-type=commonjs
。
包作者应该包括 "type"
字段,即使在所有源都是 CommonJS 的包中也是如此。
如果 Node.js 的默认类型发生变化,显式说明包的 type
将使包面向未来,它还将使构建工具和加载器更容易确定应如何解释包中的文件。
模块加载器#
Node.js 有两个系统用于解析说明符和加载模块。
有 CommonJS 模块加载器:
- 它是完全同步的。
- 它负责处理
require()
调用。 - 它是可修补的。
- 它支持文件夹作为模块。
- 当解析说明符时,如果没有找到完全的匹配,则它将尝试添加扩展名(
.js
、.json
,最后是.node
),然后尝试将文件夹作为模块解析。 - 它将
.json
视为 JSON 文本文件。 .node
文件被解释为加载了process.dlopen()
的编译插件模块。- 它将所有缺少
.json
或.node
扩展名的文件视为 JavaScript 文本文件。 - 它不能用于加载 ECMAScript 模块(尽管可以从 CommonJS 模块加载 ECMASCript 模块)。 当用于加载不是 ECMAScript 模块的 JavaScript 文本文件时,则它将作为 CommonJS 模块加载。
有 ECMAScript 模块加载器:
- 它是异步的。
- 负责处理
import
语句和import()
表达式。 - 它不是可修补的,可以使用加载器钩子自定义。
- 它不支持文件夹作为模块,必须完全指定目录索引(例如
'./startup/index.js'
)。 - 它不进行扩展名搜索。 当说明符是相对或绝对的文件 URL 时,必须提供文件扩展名。
- 它可以加载 JSON 模块,但需要导入断言。
- 它只接受 JavaScript 文本文件的
.js
、.mjs
和.cjs
扩展名。 - 它可以用来加载 JavaScript CommonJS 模块。
这样的模块通过
cjs-module-lexer
来尝试识别命名的导出,如果可以通过静态分析确定的话是可用的。 导入的 CommonJS 模块将其 URL 转换为绝对路径,然后通过 CommonJS 模块加载器加载。
package.json 和文件扩展名#
在包中,package.json
"type"
字段定义了 Node.js 应该如何解释 .js
文件。
如果 package.json
文件没有 "type"
字段,则 .js
文件将被视为 CommonJS。
"module"
的 package.json
"type"
值告诉 Node.js 将该包中的 .js
文件解释为使用 ES 模块语法。
"type"
字段不仅适用于初始入口点 (node my-app.js
),还适用于 import
语句和 import()
表达式引用的文件。
// my-app.js 被当做 ES 模块,
// 因为在同一个文件夹中有 package.json 文件与 "type": "module"。
import './startup/init.js';
// 作为 ES 模块加载,因为 ./startup 不包含 package.json 文件,
// 因此从上一层继承了 "type" 值。
import 'commonjs-package';
// 作为 CommonJS 加载,因为 ./node_modules/commonjs-package/package.json
// 缺少 "type" 字段或包含 "type": "commonjs"。
import './node_modules/commonjs-package/index.js';
// 作为 CommonJS 加载,因为 ./node_modules/commonjs-package/package.json
// 缺少 "type" 字段或包含 "type": "commonjs"。
以 .mjs
结尾的文件总是作为 ES 模块加载,而不管最近的父级 package.json
。
以 .cjs
结尾的文件总是作为 CommonJS 加载,而不管最近的父级 package.json
。
import './legacy-file.cjs';
// 作为 CommonJS 加载,因为 .cjs 总是作为 CommonJS 加载。
import 'commonjs-package/src/index.mjs';
// 作为 ES 模块加载,因为 .mjs 总是作为 ES 模块加载。
.mjs
和 .cjs
扩展可用于在同一个包中混合类型:
-
在
"type": "module"
包中,Node.js 可以通过使用.cjs
扩展名命名它来指示将特定文件解释为 CommonJS(因为.js
和.mjs
文件都被视为"module"
包中的 ES 模块) -
在
"type": "commonjs"
包中,Node.js 可以被指示将特定文件解释为 ES 模块,方法是使用.mjs
扩展名命名它(因为.js
和.cjs
文件都被视为"commonjs"
包中的 CommonJS)。
--input-type 标志#
作为参数传给 --eval
(或 -e
),或通过 STDIN
管道传输到 node
的字符串,在设置 --input-type=module
标志时被视为 ES 模块。
node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"
echo "import { sep } from 'node:path'; console.log(sep);" | node --input-type=module
为了完整起见,还有 --input-type=commonjs
,用于显式地将字符串输入作为 CommonJS 运行。
如果未指定 --input-type
,这是默认行为。
确定包管理器#
虽然所有 Node.js 项目在发布后都可以由所有包管理器安装,但他们的开发团队通常需要使用特定的包管理器。 为了使这个过程更容易,Node.js 附带了一个名为 Corepack 的工具,旨在使所有包管理器在您的环境中透明可用,只要您安装了 Node.js
默认情况下,Corepack 不会强制执行任何特定的包管理器,而是使用与每个 Node.js 版本相关联的通用“最后一次正确”版本,但您可以通过在项目的 package.json
中设置 "packageManager"
字段来改善这种体验。
包的入口#
在包的 package.json
文件中,两个字段可以定义包的入口点:"main"
和 "exports"
。
这两个字段都适用于 ES 模块和 CommonJS 模块入口点。
所有版本的 Node.js 都支持 "main"
字段,但它的功能有限:它只定义了包的主要入口点。
"exports"
提供了 "main"
的现代替代方案,允许定义多个入口点,支持环境之间的条件入口解析,并防止除了 "exports"
中定义的入口点之外的任何其他入口点。
此封装允许模块作者清楚地为他们的包定义公共接口。
对于针对当前支持的 Node.js 版本的新包,建议使用 "exports"
字段。
对于支持 Node.js 10 及以下的包,"main"
字段是必需的。
如果同时定义了 "exports"
和 "main"
,则在支持的 Node.js 版本中,"exports"
字段优先于 "main"
。
条件导出可以在 "exports"
中用于为每个环境定义不同的包入口点,包括包是通过 require
还是通过 import
引用。
有关在单个包中同时支持 CommonJS 和 ES 模块的更多信息,请参阅双 CommonJS/ES 模块包章节。
现有包引入 "exports"
字段将阻止包的消费者使用任何未定义的入口点,包括 package.json
(例如 require('your-package/package.json')
。
这可能是一个突破性的变化。
为了使 "exports"
的引入不间断,请确保导出每个以前支持的入口点。
最好明确指定入口点,以便包的公共 API 定义明确。
例如,以前导出 main
、lib
、feature
和 package.json
的项目可以使用以下 package.exports
:
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/index": "./lib/index.js",
"./lib/index.js": "./lib/index.js",
"./feature": "./feature/index.js",
"./feature/index": "./feature/index.js",
"./feature/index.js": "./feature/index.js",
"./package.json": "./package.json"
}
}
或者,项目可以选择使用导出模式导出带有和不带有扩展子路径的整个文件夹:
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/*": "./lib/*.js",
"./lib/*.js": "./lib/*.js",
"./feature": "./feature/index.js",
"./feature/*": "./feature/*.js",
"./feature/*.js": "./feature/*.js",
"./package.json": "./package.json"
}
}
以上为任何次要包版本提供向后兼容性,包的未来重大更改可以适当地将导出限制为仅暴露的特定功能导出:
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./feature/*.js": "./feature/*.js",
"./feature/internal/*": null
}
}
主入口的导出#
当编写新包时,建议使用 "exports"
字段:
{
"exports": "./index.js"
}
当定义了 "exports"
字段时,则包的所有子路径都被封装,不再提供给导入器。
例如,require('pkg/subpath.js')
抛出 ERR_PACKAGE_PATH_NOT_EXPORTED
错误。
这种导出的封装为工具的包接口以及处理包的语义版本升级提供了更可靠的保证。
这不是强封装,因为直接要求包的任何绝对子路径,例如 require('/path/to/node_modules/pkg/subpath.js')
仍然会加载 subpath.js
。
所有当前支持的 Node.js 版本和现代构建工具都支持 "exports"
字段。
对于使用旧版本 Node.js 或相关构建工具的项目,可以通过在 "exports"
旁边包含指向同一模块的 "main"
字段来实现兼容性:
{
"main": "./index.js",
"exports": "./index.js"
}
子路径的导出#
当使用 "exports"
字段时,可以通过将主入口点视为 "."
子路径来定义自定义子路径以及主入口点:
{
"exports": {
".": "./index.js",
"./submodule.js": "./src/submodule.js"
}
}
现在消费者只能导入 "exports"
中定义的子路径:
import submodule from 'es-module-package/submodule.js';
// 加载 ./node_modules/es-module-package/src/submodule.js
而其他子路径会出错:
import submodule from 'es-module-package/private-module.js';
// 抛出 ERR_PACKAGE_PATH_NOT_EXPORTED
Extensions in subpaths#
包作者应在其导出中提供扩展 (import 'pkg/subpath.js'
) 或无扩展 (import 'pkg/subpath'
) 子路径。
这确保每个导出的模块只有一个子路径,以便所有依赖项导入相同的一致说明符,使消费者清楚地了解包合同并简化包子路径的完成。
传统上,包倾向于使用无扩展名风格,它具有可读性和掩盖包中文件的真实路径的好处。
随着导入映射现在为浏览器和其他 JavaScript 运行时中的包解析提供标准,使用无扩展风格可能会导致导入映射定义臃肿。 显式的文件扩展名可以通过启用导入映射来避免此问题,以利用包文件夹映射在可能的情况下映射多个子路径,而不是每个包子路径导出单独的映射条目。 这也反映了在相对和绝对导入说明符中使用完整说明符路径的要求。
导出的语法糖#
如果 "."
导出是唯一的导出,则 "exports"
字段为这种情况提供了语法糖,即直接的 "exports"
字段值。
{
"exports": {
".": "./index.js"
}
}
可以写成:
{
"exports": "./index.js"
}
子路径的导入#
除了 "exports"
字段之外,还有一个包 "imports"
字段用于创建仅适用于包本身的导入说明符的私有映射。
"imports"
字段中的条目必须始终以 #
开头,以确保它们与外部包说明符消除歧义。
例如,可以使用导入字段来获得内部模块条件导出的好处:
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}
其中 import '#dep'
没有得到外部包 dep-node-native
的解析(依次包括其导出),而是获取了相对于其他环境中的包的本地文件 ./dep-polyfill.js
。
与 "exports"
字段不同,"imports"
字段允许映射到外部包。
导入字段的解析规则与导出字段类似。
子路径的模式#
对于具有少量导出或导入的包,我们建议显式地列出每个导出子路径条目。
但是对于具有大量子路径的包,这可能会导致 package.json
膨胀和维护问题。
对于这些用例,可以使用子路径导出模式:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*.js": "./src/features/*.js"
},
"imports": {
"#internal/*.js": "./src/internal/*.js"
}
}
*
映射公开嵌套的子路径,因为它只是字符串替换语法。
然后,右侧 *
的所有实例都将替换为该值,包括它是否包含任何 /
分隔符。
import featureX from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js
import featureY from 'es-module-package/features/y/y.js';
// 加载 ./node_modules/es-module-package/src/features/y/y.js
import internalZ from '#internal/z.js';
// 加载 ./node_modules/es-module-package/src/internal/z.js
这是直接静态匹配和替换,无需对文件扩展名进行任何特殊处理。
在映射两边包含 "*.js"
限制了暴露的包导出到只有 JS 文件。
导出的静态可枚举属性由导出模式维护,因为可以通过将右侧目标模式视为针对包内文件列表的 **
glob 来确定包的各个导出。
因为导出目标中禁止 node_modules
路径,所以这个扩展只依赖包本身的文件。
要从模式中排除私有子文件夹,可以使用 null
目标:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*.js": "./src/features/*.js",
"./features/private-internal/*": null
}
}
import featureInternal from 'es-module-package/features/private-internal/m.js';
// 抛出: ERR_PACKAGE_PATH_NOT_EXPORTED
import featureX from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js
条件导出#
条件导出提供了一种根据特定条件映射到不同路径的方法。 CommonJS 和 ES 模块导入都支持它们。
比如,包想要为 require()
和 import
提供不同的 ES 模块导出可以这样写:
// package.json
{
"exports": {
"import": "./index-module.js",
"require": "./index-require.cjs"
},
"type": "module"
}
Node.js 实现了以下条件,按从最具体到最不具体的顺序列出,因为应该定义条件:
"node-addons"
- 类似于"node"
并匹配任何 Node.js 环境。 此条件可用于提供使用原生 C++ 插件的入口点,而不是更通用且不依赖原生插件的入口点。 可以通过--no-addons
标志禁用此条件。"node"
- 匹配任何 Node.js 环境。 可以是 CommonJS 或 ES 模块文件。 在大多数情况下,不需要明确调用 Node.js 平台。"import"
- 当包通过import
或import()
,或者通过 ECMAScript 模块加载器的任何顶层导入或解析操作加载时匹配。 无论目标文件的模块格式如何,都适用。 始终与"require"
互斥。"require"
- 当包通过require()
加载时匹配。 引用的文件应该可以用require()
加载,尽管无论目标文件的模块格式如何,条件都匹配。 预期的格式包括 CommonJS、JSON 和原生插件,但不包括 ES 模块,因为require()
不支持它们。 始终与"import"
互斥。"default"
- 始终匹配的通用后备。 可以是 CommonJS 或 ES 模块文件。 此条件应始终放在最后。
在 "exports"
对象中,键顺序很重要。
在条件匹配过程中,较早的条目具有更高的优先级并优先于较晚的条目。
一般规则是条件应该按照对象顺序从最具体到最不具体。
使用 "import"
和 "require"
条件会导致一些危害,在双 CommonJS/ES 模块包章节中有进一步的解释。
"node-addons"
条件可用于提供使用原生 C++ 插件的入口点。
但是,可以通过 --no-addons
标志禁用此条件。
当使用 "node-addons"
时,建议将 "default"
视为提供更通用入口点的增强功能,例如使用 WebAssembly 而不是原生插件。
条件导出也可以扩展为导出子路径,例如:
{
"exports": {
".": "./index.js",
"./feature.js": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
定义了一个包,其中 require('pkg/feature.js')
和 import 'pkg/feature.js'
可以在 Node.js 和其他 JS 环境之间提供不同的实现。
当使用环境分支时,总是尽可能包含 "default"
条件。
提供 "default"
条件可确保任何未知的 JS 环境都能够使用此通用实现,这有助于避免这些 JS 环境必须伪装成现有环境以支持具有条件导出的包。
出于这个原因,使用 "node"
和 "default"
条件分支通常比使用 "node"
和 "browser"
条件分支更可取。
嵌套的条件#
除了直接映射,Node.js 还支持嵌套条件对象。
例如,要定义一个包,它只有双模式入口点用于 Node.js 而不是浏览器:
{
"exports": {
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
},
"default": "./feature.mjs"
}
}
条件继续按顺序与平面条件匹配。
如果嵌套条件没有任何映射,它将继续检查父条件的剩余条件。
通过这种方式,嵌套条件的行为类似于嵌套的 JavaScript if
语句。
处理用户条件#
运行 Node.js 时,可以使用 --conditions
标志添加自定义用户条件:
node --conditions=development index.js
然后将解析包导入和导出中的 "development"
条件,同时根据需要解析现有的 "node"
、"node-addons"
、"default"
、"import"
和 "require"
条件。
可以使用重复标志设置任意数量的自定义条件。
社区条件定义#
除了在 Node.js 核心中实现的 "import"
、"require"
、"node"
、"node-addons"
和 "default"
条件之外的条件字符串默认被忽略。
其他平台可能实现其他条件,用户条件可以通过--conditions
/ -C
标识在 Node.js 中启用。
由于自定义的包条件需要明确定义以确保正确使用,因此下面提供了常见的已知包条件及其严格定义的列表,以协助生态系统协调。
"types"
- 类型系统可以使用它来解析给定导出的类型文件。 此条件应始终首先包含在内。"deno"
- 表示 Deno 平台的变体。"browser"
- 任何网络浏览器环境。"development"
- 可用于定义仅开发环境入口点,例如提供额外的调试上下文(例如在开发模式下运行时更好的错误消息)。 必须始终与"production"
互斥。"production"
- 可用于定义生产环境入口点。 必须始终与"development"
互斥。
可以通过向本节的 Node.js 文档创建拉取请求,将新的条件定义添加到此列表中。 在此处列出新条件定义的要求是:
- 对于所有实现者来说,定义应该是清晰明确的。
- 为什么需要条件的用例应该清楚地证明。
- 应该存在足够的现有实现用法。
- 条件名称不应与另一个条件定义或广泛使用的条件冲突。
- 条件定义的列表应该为生态系统提供协调效益,否则这是不可能的。 例如,对于特定于公司或特定于应用程序的条件,情况不一定如此。
上述定义可能会在适当的时候移到专门的条件仓库中。
使用名称来引用包#
在一个包中,包的 package.json
"exports"
字段中定义的值可以通过包的名称引用。
例如,假设 package.json
是:
// package.json
{
"name": "a-package",
"exports": {
".": "./index.mjs",
"./foo.js": "./foo.js"
}
}
然后该包中的任何模块都可以引用包本身中的导出:
// ./a-module.mjs
import { something } from 'a-package'; // 从 ./index.mjs 导入 "something"。
自引用仅在 package.json
具有 "exports"
时可用,并且只允许导入 "exports"
(在 package.json
中)允许的内容。
所以下面的代码,给定前面的包,会产生运行时错误:
// ./another-module.mjs
// 从 ./m.mjs 导入 "another"。
// 失败,因为 "package.json" "exports" 字段
// 不提供名为 "./m.mjs" 的导出。
import { another } from 'a-package/m.mjs';
在 ES 模块和 CommonJS 模块中使用 require
时也可以使用自引用。
例如,这段代码也可以工作:
// ./a-module.js
const { something } = require('a-package/foo.js'); // 从 ./foo.js 加载。
最后,自引用也适用于作用域包。 例如,这段代码也可以工作:
// package.json
{
"name": "@my/package",
"exports": "./index.js"
}
// ./index.js
module.exports = 42;
// ./other.js
console.log(require('@my/package'));
$ node other.js
42
子路径文件夹映射#
在支持子路径模式之前,尾随 "/"
后缀用于支持文件夹映射:
{
"exports": {
"./features/": "./features/"
}
}
此特性将在未来版本中删除。
而是,使用直接的子路径模式:
{
"exports": {
"./features/*": "./features/*.js"
}
}
模式相对于文件夹导出的好处在于,消费者始终可以导入包,而无需子路径文件扩展名。
双 CommonJS/ES 模块包#
在 Node.js 中引入对 ES 模块的支持之前,包作者的一种常见模式是在他们的包中包含 CommonJS 和 ES 模块 JavaScript 源代码,其中 package.json
"main"
指定了 CommonJS 入口点,而 package.json
"module"
指定了 ES模块入口点。
这使 Node.js 能够运行 CommonJS 入口点,而构建工具(例如捆绑器)使用 ES 模块入口点,因为 Node.js 忽略(并且仍然忽略)顶层 "module"
字段。
Node.js 现在可以运行 ES 模块入口点,并且一个包可以同时包含 CommonJS 和 ES 模块入口点(通过单独的说明符,例如 'pkg'
和 'pkg/es-module'
,或者通过条件导出在同一个说明符中)。
与 "module"
仅由打包程序使用的场景不同,或者在 Node.js 评估之前将 ES 模块文件动态转换为 CommonJS,ES 模块入口点引用的文件被评估为 ES 模块。
双包的危害#
当应用程序使用提供 CommonJS 和 ES 模块源的包时,如果包的两个版本都被加载,则存在某些错误的风险。
此潜力来自于 const pkgInstance = require('pkg')
创建的 pkgInstance
与 import pkgInstance from 'pkg'
创建的 pkgInstance
(或像 'pkg/module'
这样的替代主路径)不同的事实。
这是“双包风险”,同一包的两个版本可以在同一个运行时环境中加载。
虽然应用程序或包不太可能有意直接加载两个版本,但应用程序加载一个版本而应用程序的依赖项加载另一个版本是很常见的。
这种危险可能发生,因为 Node.js 支持混合 CommonJS 和 ES 模块,并可能导致意外行为。
如果包主导出是一个构造函数,两个版本创建的实例的 instanceof
比较返回 false
,如果导出是一个对象,添加到一个的属性(如 pkgInstance.foo = 3
)在另一个上不存在。
这与 import
和 require
语句分别在全 CommonJS 或全 ES 模块环境中的工作方式不同,因此令用户感到惊讶。
它也不同于用户在通过 Babel 或 esm
等工具使用转译时所熟悉的行为。
在避免或最小化危害的同时编写双包#
首先,当一个包同时包含 CommonJS 和 ES 模块源并且这两个源都通过单独的主入口点或导出路径提供以在 Node.js 中使用时,就会发生上一节中描述的危险。
一个包可能被写成任何版本的 Node.js 只接收 CommonJS 源,并且包可能包含的任何单独的 ES 模块源仅用于其他环境,例如浏览器。
这样的包可以被任何版本的 Node.js 使用,因为 import
可以引用 CommonJS 文件;但它不会提供使用 ES 模块语法的任何优点。
一个包也可能会在重大更改版本碰撞中从 CommonJS 切换到 ES 模块语法。 这有一个缺点,即最新版本的包只能在支持 ES 模块的 Node.js 版本中使用。
每种模式都有权衡,但有两种广泛的方法可以满足以下条件:
- 该软件包可通过
require
和import
使用。 - 该包在当前 Node.js 和不支持 ES 模块的旧版本 Node.js 中都可用。
- 包主入口点,例如
'pkg'
可以被require
用来解析 CommonJS 文件,也可以被import
用来解析 ES 模块文件。 (对于导出的路径也是如此,例如'pkg/feature'
。) - 该包提供命名导出,例如
import { name } from 'pkg'
而不是import pkg from 'pkg'; pkg.name
。 - 该包可能在其他 ES 模块环境中可用,例如浏览器。
- 避免或最小化上一节中描述的危害。
方法1:使用 ES 模块封装器#
在 CommonJS 中编写包或将 ES 模块源代码转换为 CommonJS,并创建定义命名导出的 ES 模块封装文件。
使用条件导出, import
使用 ES 模块封装器,require
使用 CommonJS 入口点。
// ./node_modules/pkg/package.json
{
"type": "module",
"exports": {
"import": "./wrapper.mjs",
"require": "./index.cjs"
}
}
前面的示例使用显式扩展 .mjs
和 .cjs
。
如果你的文件使用 .js
扩展名,"type": "module"
会导致这些文件被视为 ES 模块,就像 "type": "commonjs"
会导致它们被视为 CommonJS。
参阅启用。
// ./node_modules/pkg/index.cjs
exports.name = 'value';
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;
在这个例子中,import { name } from 'pkg'
中的 name
与 const { name } = require('pkg')
中的 name
是相同的单例。
因此,当比较两个 name
时,===
返回 true
,避免了发散说明符的危险。
如果模块不是简单的命名导出列表,而是包含独特的函数或对象导出,如 module.exports = function () { ... }
,或者如果需要封装器支持 import pkg from 'pkg'
模式,则封装器将被编写为可选地导出默认值以及任何命名的导出:
import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule;
此方法适用于以下任何用例:
- 该包目前是用 CommonJS 编写的,作者不希望将其重构为 ES 模块语法,而是希望为 ES 模块使用者提供命名导出。
- 该包还有其他依赖它的包,最终用户可能会同时安装这个包和那些其他包。
比如
utilities
包直接在应用中使用,utilities-plus
包给utilities
增加了一些功能。 因为封装器导出了底层的 CommonJS 文件,所以utilities-plus
是用 CommonJS 还是 ES 模块语法编写的并不重要;无论哪种方式都可以。 - 包存储内部状态,包作者宁愿不重构包以隔离其状态管理。 请参阅下一章节。
此方法的变体不需要消费者有条件导出,可以添加一个导出,例如 "./module"
,指向包的全 ES 模块语法版本。
如果用户确定 CommonJS 版本不会在应用程序的任何地方加载,例如通过依赖项,或者如果可以加载 CommonJS 版本但不影响 ES 模块版本(例如, 因为包是无状态的):
// ./node_modules/pkg/package.json
{
"type": "module",
"exports": {
".": "./index.cjs",
"./module": "./wrapper.mjs"
}
}
方法2:隔离状态#
package.json
文件可以直接定义单独的 CommonJS 和 ES 模块入口点:
// ./node_modules/pkg/package.json
{
"type": "module",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
如果包的 CommonJS 和 ES 模块版本是等效的,则可以做到这一点,例如因为一个是另一个的转译输出;并且包的状态管理被仔细隔离(或包是无状态的)。
状态是一个问题的原因是因为包的 CommonJS 和 ES 模块版本都可能在应用程序中使用;例如,用户的应用程序代码可以 import
ES 模块版本,而依赖项 require
CommonJS 版本。
如果发生这种情况,包的两个副本将被加载到内存中,因此将出现两个不同的状态。
这可能会导致难以解决的错误。
除了编写无状态包(例如,如果 JavaScript 的 Math
是一个包,它将是无状态的,因为它的所有方法都是静态的),还有一些方法可以隔离状态,以便在可能加载的 CommonJS 和 ES 模块之间共享它包的实例:
-
如果可能,在实例化对象中包含所有状态。 比如 JavaScript 的
Date
,需要实例化包含状态;如果是包,会这样使用:import Date from 'date'; const someDate = new Date(); // someDate 包含状态;Date 不包含
new
关键字不是必需的;包的函数可以返回新的对象,或修改传入的对象,以保持包外部的状态。 -
在包的 CommonJS 和 ES 模块版本之间共享的一个或多个 CommonJS 文件中隔离状态。 比如 CommonJS 和 ES 模块入口点分别是
index.cjs
和index.mjs
:// ./node_modules/pkg/index.cjs const state = require('./state.cjs'); module.exports.state = state;
// ./node_modules/pkg/index.mjs import state from './state.cjs'; export { state };
即使
pkg
在应用程序中通过require
和import
使用(例如,通过应用程序代码中的import
和依赖项通过require
)pkg
的每个引用都将包含相同的状态;并且从任一模块系统修改该状态将适用二者皆是。
任何附加到包单例的插件都需要分别附加到 CommonJS 和 ES 模块单例。
此方法适用于以下任何用例:
- 该包目前是用 ES 模块语法编写的,包作者希望在支持此类语法的任何地方使用该版本。
- 包是无状态的,或者它的状态可以很容易地被隔离。
- 该包不太可能有其他依赖它的公共包,或者如果有,则该包是无状态的,或者具有不需要在依赖项之间或与整个应用程序共享的状态。
即使处于隔离状态,在 CommonJS 和 ES 模块版本之间仍然存在可能执行额外代码的成本。
与之前的方法一样,这种方法的变体不需要消费者有条件的导出,可以添加一个导出,例如 "./module"
,指向包的全 ES 模块语法版本:
// ./node_modules/pkg/package.json
{
"type": "module",
"exports": {
".": "./index.cjs",
"./module": "./index.mjs"
}
}
Node.js package.json 字段定义#
本节描述了 Node.js 运行时使用的字段。 其他工具(例如 npm)使用 Node.js 忽略且未在此处记录的其他字段。
package.json
文件中的以下字段在 Node.js 中使用:
"name"
- 当包中使用命名导入时相关。 也被包管理器用作包的名称。"main"
- 加载包时的默认模块,如果没有指定导出,并且在引入导出之前的 Node.js 版本中。"packageManager"
- 为包做出贡献时推荐的包管理器。 由 Corepack 垫片利用。"type"
- 决定是否将.js
文件加载为 CommonJS 或 ES 模块的包类型。"exports"
- 包导出和条件导出。 当存在时,限制可以从包中加载哪些子模块。"imports"
- 包导入,供包本身内的模块使用。
"name"
#
- 类型: <string>
{
"name": "package-name"
}
"name"
字段定义了你的包名。
发布到 npm 仓库需要满足特定要求的名称。
除了 "exports"
字段外,"name"
字段还可用于自引用使用其名称的包。
"main"
#
- 类型: <string>
{
"main": "./index.js"
}
当通过 node_modules
查找按名称导入时,则 "main"
字段定义了包的入口点。
其值为路径。
当包具有 "exports"
字段时,则在按名称导入包时,这将优先于 "main"
字段。
它还定义了通过 require()
加载包目录时使用的脚本。
// 这解析为 ./path/to/directory/index.js。
require('./path/to/directory');
"packageManager"
#
- 类型: <string>
{
"packageManager": "<package manager name>@<version>"
}
"packageManager"
字段定义了在处理当前项目时预期使用的包管理器。
它可以设置为任何支持的包管理器,并确保您的团队使用完全相同的包管理器版本,而无需安装 Node.js 以外的任何其他东西。
该领域目前处于试验阶段,需要选择加入;有关该过程的详细信息,请查看 Corepack 页面。
"type"
#
- 类型: <string>
"type"
字段定义了 Node.js 用于所有 .js
文件的模块格式,这些 .js
文件将该 package.json
文件作为其最近的父文件。
当最近的父 package.json
文件包含值为 "module"
的顶级字段 "type"
时,以 .js
结尾的文件将作为 ES 模块加载。
最近的父 package.json
定义为在当前文件夹中搜索时找到的第一个 package.json
,该文件夹的父文件夹,依此类推,直到到达 node_modules 文件夹或卷根。
// package.json
{
"type": "module"
}
# 在与前面的 package.json 相同的文件夹中
node my-app.js # 作为 ES 模块运行
如果最近的父 package.json
缺少 "type"
字段,或包含 "type": "commonjs"
,则 .js
文件将被视为 CommonJS。
如果到达卷根目录但未找到 package.json
,则将 .js
文件视为 CommonJS。
如果最近的父 package.json
包含 "type": "module"
,则 .js
文件的 import
语句被视为 ES 模块。
// my-app.js, 同上示例的一部分
import './startup.js'; // 由于 package.json 加载为 ES 模块
无论 "type"
字段的值如何,.mjs
文件始终被视为 ES 模块,而 .cjs
文件始终被视为 CommonJS。
"exports"
#
- 类型: <Object> | <string> | <string[]>
{
"exports": "./index.js"
}
"exports"
字段允许定义包的入口点,当通过 node_modules
查找或自引用加载到其自身的名称的名称导入时。
Node.js 12+ 支持它作为 "main"
的替代方案,它可以支持定义子路径导出和条件导出,同时封装内部未导出的模块。
条件导出也可以在 "exports"
中用于为每个环境定义不同的包入口点,包括包是通过 require
还是通过 import
引用。
"exports"
中定义的所有路径必须是以 ./
开头的相对文件 URL。
"imports"
#
- 类型: <Object>
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}
导入字段中的条目必须是以 #
开头的字符串。
包导入允许映射到外部包。
此字段为当前包定义了子路径导入。