- 断言测试
- 异步上下文跟踪
- 异步钩子
- 缓冲(Buffer)
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入 Node环境
- 子进程(Child processes)
- 集群(Cluster)
- 命令行选项
- 控制台(Console)
- 核心包(Corepack)
- 加密(Crypto)
- 调试器(Debugger)
- 已弃用的 API
- 诊断通道(Diagnostics Channel)
- 域名系统(DNS)
- 域(Domain)
- 错误(Errors)
- 事件(Events)
- 文件系统(File system)
- 全局变量(Globals)
- HTTP
- HTTP/2
- HTTPS
- 检查器(Inspector)
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:module
API - 模块:packages 模块
- 网络(Net)
- 系统(OS)
- 路径(Path)
- 性能挂钩(Performance hooks)
- 性能挂钩(Permissions)
- 进程(Process)
- Punycode 国际化域名编码
- 查询字符串(Query strings)
- 命令行库(Readline)
- REPL 交互式编程环境
- 诊断报告
- 单个可执行应用程序
- Stream 流
- 字符串解码器
- 单元测试
- 定时器(Timers)
- 传输层安全/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用程序
- V8
- 虚拟机
- WebAssembly
- Web加密 API(Web Crypto API)
- 网络流 API(Web Streams API)
- 工作线程(Worker threads)
- zlib
Node.js v18.18.2 文档
- Node.js v18.18.2
- ► 目录
-
►
索引
- 断言测试
- 异步上下文跟踪
- 异步钩子
- 缓冲(Buffer)
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入 Node环境
- 子进程(Child processes)
- 集群(Cluster)
- 命令行选项
- 控制台(Console)
- 核心包(Corepack)
- 加密(Crypto)
- 调试器(Debugger)
- 已弃用的 API
- 诊断通道(Diagnostics Channel)
- 域名系统(DNS)
- 域(Domain)
- 错误(Errors)
- 事件(Events)
- 文件系统(File system)
- 全局变量(Globals)
- HTTP
- HTTP/2
- HTTPS
- 检查器(Inspector)
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:module
API - 模块:packages 模块
- 网络(Net)
- 系统(OS)
- 路径(Path)
- 性能挂钩(Performance hooks)
- 性能挂钩(Permissions)
- 进程(Process)
- Punycode 国际化域名编码
- 查询字符串(Query strings)
- 命令行库(Readline)
- REPL 交互式编程环境
- 诊断报告
- 单个可执行应用程序
- Stream 流
- 字符串解码器
- 单元测试
- 定时器(Timers)
- 传输层安全/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用程序
- V8
- 虚拟机
- WebAssembly
- Web加密 API(Web Crypto API)
- 网络流 API(Web Streams API)
- 工作线程(Worker threads)
- zlib
- ► 其他版本
- ► 选项
模块:packages 模块#
介绍#
包是由package.json
文件描述的文件夹树。该包由包含package.json
文件的文件夹和所有子文件夹组成,直到下一个包含另一个package.json
文件的文件夹或名为node_modules
的文件夹。
本页面为包作者编写package.json
文件提供指导,并提供Node.js 定义的package.json
字段的参考。
确定模块系统#
当传递给node
作为初始输入时,或者被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()
表达式:
-
扩展名为
.cjs
的文件。 -
当最近的父
package.json
文件包含值为"commonjs"
的顶级字段"type"
时,具有.js
扩展名的文件。 -
字符串作为参数传入
--eval
或--print
,或通过STDIN
通过管道传送到node
,标志为--input-type=commonjs
。
包作者应该包含"type"
字段,即使在所有源都是 CommonJS 的包中也是如此。明确包的type
将使包能够适应未来,以防 Node.js 的默认类型发生变化,并且还可以让构建工具和加载器更轻松地确定文件中的文件方式包应该被解释。
模块加载器#
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。
package.json
"type"
值"module"
告诉 Node.js 将该包中的.js
文件解释
为使用ES 模块语法。
"type"
字段不仅适用于初始入口点 ( node my-app.js
),还适用于import
语句和import()
表达式引用的文件。
// my-app.js, treated as an ES module because there is a package.json
// file in the same folder with "type": "module".
import './startup/init.js';
// Loaded as ES module since ./startup contains no package.json file,
// and therefore inherits the "type" value from one level up.
import 'commonjs-package';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".
import './node_modules/commonjs-package/index.js';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".
以.mjs
结尾的文件始终作为ES 模块加载,无论最近的父文件package.json
是什么。
以.cjs
结尾的文件始终作为CommonJS加载,无论最近的父文件package.json
是什么。
import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always loaded as CommonJS.
import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always loaded as ES module.
.mjs
和.cjs
扩展可用于在同一包中混合类型:
-
在
"type": "module"
包中,可以指示 Node.js 将特定文件解释为CommonJS,方法是使用.cjs
扩展名命名该文件(因为.js
和.mjs
文件被视为"module"
包中的 ES 模块)。 -
在
"type": "commonjs"
包中,可以指示 Node.js 将特定文件解释为ES 模块,方法是使用.mjs
扩展名命名该文件(因为.js
和.cjs
文件被视为"commonjs"
包中的 CommonJS)。
--input-type
标志#
作为参数传入--eval
(或-e
)或通过STDIN
通过
管道传递到node
的字符串,在以下情况下被视为ES 模块: --input-type=module
标志已设置。
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 版本关联的通用“最后一次正确”版本,但您可以通过在项目的项目中设置"packageManager"
字段来改善此体验package.json
。
包入口点#
在包的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
错误。
这种导出封装为工具的包接口以及处理包的 semver 升级提供了更可靠的保证。它不是一个强封装,因为直接要求包的任何绝对子路径(例如
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';
// Loads ./node_modules/es-module-package/src/submodule.js
而其他子路径会出错:
import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
子路径中的扩展#
包作者应在其导出中提供扩展 ( import 'pkg/subpath.js'
) 或无扩展 ( import 'pkg/subpath'
) 子路径。这确保每个导出的模块只有一个子路径,以便所有依赖项导入相同的一致说明符,使包契约对消费者来说清晰并简化包子路径完成。
传统上,包倾向于使用无扩展名风格,这种风格具有可读性和掩盖包内文件的真实路径的优点。
现在,导入映射为浏览器和其他 JavaScript 运行时中的包解析提供了标准,使用无扩展样式可能会导致导入映射定义臃肿。显式文件扩展名可以避免此问题,方法是使导入映射能够利用包文件夹映射来尽可能映射多个子路径,而不是为每个包子路径导出使用单独的映射条目。 这也反映了在相对和绝对导入说明符中使用完整说明符路径的要求。
导出sugar#
如果"."
导出是唯一导出,则"exports"
字段为这种情况提供sugar,即直接"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';
// Loads ./node_modules/es-module-package/src/features/x.js
import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js
import internalZ from '#internal/z.js';
// Loads ./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';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED
import featureX from 'es-module-package/features/x.js';
// Loads ./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"
- 可由键入系统用来解析给定导出的键入文件。此条件应始终首先包含在内。"browser"
- 任何网络浏览器环境。"development"
- 可用于定义仅开发环境入口点,例如提供额外的调试上下文,例如在开发模式下运行时提供更好的错误消息。必须始终与"production"
互斥。"production"
- 可用于定义生产环境入口点。必须始终与"development"
互斥。
对于其他运行时,特定于平台的条件键定义由WinterCG在运行时键提案规范中维护。
通过为本节的 Node.js 文档创建拉取请求,可以将新的条件定义添加到此列表中。此处列出新条件定义的要求是:
- 对于所有实施者来说,定义应该是清晰且明确的。
- 应明确说明为什么需要该条件的用例。
- 应该存在足够的现有实施用途。
- 条件名称不应与其他条件定义或广泛使用的条件冲突。
- 条件定义的列出应该为生态系统提供协调效益,否则这是不可能的。例如,对于特定于公司或特定于应用程序的条件来说,情况不一定如此。
- 该条件应该是 Node.js 用户期望它出现在 Node.js 核心文档中。
"types"
条件就是一个很好的例子:它并不真正属于运行时键提案,但很适合 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'; // Imports "something" from ./index.mjs.
仅当package.json
具有"exports"
时,自引用才可用,并且仅允许导入"exports"
(在package.json
中)允许的内容。因此,考虑到前面的包,下面的代码将生成运行时错误:
// ./another-module.mjs
// Imports "another" from ./m.mjs. Fails because
// the "package.json" "exports" field
// does not provide an export named "./m.mjs".
import { another } from 'a-package/m.mjs';
在 ES 模块和 CommonJS 模块中使用require
时也可以使用自引用。例如,此代码也将起作用:
// ./a-module.js
const { something } = require('a-package/foo.js'); // Loads from ./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
双 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"
仅由捆绑器使用或 ES 模块文件在 Node.js 评估之前动态转换为 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 模块包装文件。使用
条件导出,ES 模块包装器用于import
,而 CommonJS 入口点用于require
。
// ./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 模块语法版本。用户可以通过import 'pkg/module'
使用此功能,并确定 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 contains state; Date does not
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 忽略的附加字段,并且此处未记录这些字段。
Node.js 中使用package.json
文件中的以下字段:
"name"
- 在包中使用命名导入时相关。包管理器也将其用作包的名称。"main"
- 如果未指定导出,以及在引入导出之前的 Node.js 版本中,加载包时的默认模块。"packageManager"
- 为包做贡献时推荐的包管理器。由Corepack垫片利用。"type"
- 包类型决定是否将.js
文件加载为 CommonJS 或 ES 模块。"exports"
- 包导出和条件导出。如果存在,则限制可以从包内加载哪些子模块。"imports"
- 包导入,供包本身内的模块使用。
"name"
#
- 类型:<字符串>
{
"name": "package-name"
}
"name"
字段定义您的包的名称。发布到
npm注册表需要满足特定要求的名称
。
除了"exports"
字段之外,还可以使用"name"
字段来
使用包的名称自引用包。
"main"
#
- 类型:<字符串>
{
"main": "./index.js"
}
"main"
字段定义通过node_modules
查找按名称导入时包的入口点。它的值是一条路径。
当包具有"exports"
字段时,在按名称导入包时,该字段将优先于
"main"
字段。
它还定义了通过require()
加载包目录时使用的脚本。
// This resolves to ./path/to/directory/index.js.
require('./path/to/directory');
"packageManager"
#
- 类型:<字符串>
{
"packageManager": "<package manager name>@<version>"
}
"packageManager"
字段定义在处理当前项目时预期使用哪个包管理器。它可以设置为任何
受支持的包管理器,并将确保您的团队使用完全相同的包管理器版本,而无需安装除 Node.js 之外的任何其他东西。
该字段目前处于实验阶段,需要选择加入;有关该过程的详细信息,请查看 Corepack页面。
"type"
#
- 类型:<字符串>
"type"
字段定义 Node.js 对所有以该package.json
文件作为最近父级的.js
文件使用的模块格式
。
当最近的父
package.json
文件包含值为
"module"
的顶级字段 "type"
时,以.js
结尾的文件将作为 ES 模块加载。
最近的父级package.json
定义为在当前文件夹、该文件夹的父级等中搜索时找到的第一个package.json
,依此类推,直到到达 node_modules 文件夹或卷根目录。
// package.json
{
"type": "module"
}
# In same folder as preceding package.json
node my-app.js # Runs as ES module
如果最近的父级package.json
缺少"type"
字段,或包含
"type": "commonjs"
,则.js
文件将被视为CommonJS。如果到达卷根并且未找到package.json
,则.js
文件将被视为
CommonJS。
如果最近的父文件 package.json
包含"type": "module"
,则.js
文件的 import
语句将被视为 ES 模块。
// my-app.js, part of the same example as above
import './startup.js'; // Loaded as ES module because of package.json
无论"type"
字段的值如何,.mjs
文件始终被视为 ES 模块,而.cjs
文件始终被视为 CommonJS。
"exports"
#
{
"exports": "./index.js"
}
"exports"
字段允许在通过node_modules
查找或
对其自身名称的自引用加载的名称导入时定义包的入口点。Node.js 12+ 支持它作为"main"
的替代方案,可以支持定义子路径导出
和条件导出,同时封装内部未导出的模块。
条件导出还可以在"exports"
中使用,为每个环境定义不同的包入口点,包括是否通过require
或import
引用包。
"exports"
中定义的所有路径必须是以
./
开头的相对文件 URL 。
"imports"
#
- 类型:<对象>
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}
导入字段中的条目必须是以#
开头的字符串。
包导入允许映射到外部包。
该字段定义当前包的子路径导入。