包的入口#
中英对照
在包的 package.json 文件中,有两个字段可以定义包的入口点:"main" 和 "exports"。
所有版本的 Node.js 都支持 "main" 字段,但它的功能有限:它只定义了包的主要入口点。
"exports" 字段提供了 "main" 的替代方案,其中可以定义包主入口点,同时封装包,防止除 "exports" 中定义的入口点之外的任何其他入口点。
这种封装允许模块作者为他们的包定义一个公共接口。
如果同时定义了 "exports" 和 "main",则 "exports" 字段优先于 "main"。
"exports" 不特定于 ES 模块或 CommonJS;如果 "exports" 存在,则 "main" 将被覆盖。
因此 "main" 不能用作 CommonJS 的后备,但它可以用作不支持 "exports" 字段的旧版 Node.js 的后备。
条件导出可以在 "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-mod",
"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.js": "./feature/index.js",
"./package.json": "./package.json"
}
}
或者,一个项目可以选择导出整个文件夹:
{
"name": "my-mod",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/*": "./lib/*.js",
"./feature": "./feature/index.js",
"./feature/*": "./feature/*.js",
"./package.json": "./package.json"
}
}
作为最后的手段,可以通过为包 "./*": "./*" 的根创建导出来完全禁用包封装。
这会以禁用封装和潜在的工具优势为代价公开包中的每个文件。
由于 Node.js 中的 ES 模块加载器强制使用完整说明符路径,导出根而不是明确表示条目比前面的任何一个示例都没有表现力。
不仅封装丢失,模块消费者也无法 import feature from 'my-mod/feature',因为他们需要提供完整路径 import feature from 'my-mod/feature/index.js。
主入口的导出#
中英对照
要设置包的主入口点,建议在包的 package.json 文件中同时定义 "exports" 和 "main":
{
"main": "./main.js",
"exports": "./main.js"
}
当定义了 "exports" 字段时,则包的所有子路径都被封装,不再提供给导入器。
例如,require('pkg/subpath.js') 抛出 ERR_PACKAGE_PATH_NOT_EXPORTED 错误。
这种导出的封装为工具的包接口以及处理包的语义版本升级提供了更可靠的保证。
这不是强封装,因为直接要求包的任何绝对子路径,例如 require('/path/to/node_modules/pkg/subpath.js') 仍然会加载 subpath.js。
子路径的导出#
中英对照
新增于: v12.7.0
当使用 "exports" 字段时,可以通过将主入口点视为 "." 子路径来定义自定义子路径以及主入口点:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./submodule": "./src/submodule.js"
}
}
现在消费者只能导入 "exports" 中定义的子路径:
import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js
而其他子路径会出错:
import submodule from 'es-module-package/private-module.js';
// 抛出 ERR_PACKAGE_PATH_NOT_EXPORTED
子路径的导入#
中英对照
新增于: v14.6.0, v12.19.0
除了 "exports" 字段,还可以定义内部包导入映射,这些映射仅适用于包本身内部的导入说明符。
导入字段中的条目必须始终以 # 开头,以确保它们与包说明符没有歧义。
例如,可以使用导入字段来获得内部模块条件导出的好处:
// 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" 字段允许映射到外部包。
导入字段的解析规则与导出字段类似。
子路径的模式#
中英对照
新增于: v14.13.0, v12.20.0
对于具有少量导出或导入的包,我们建议显式地列出每个导出子路径条目。
但是对于具有大量子路径的包,这可能会导致 package.json 膨胀和维护问题。
对于这些用例,可以使用子路径导出模式:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js"
},
"imports": {
"#internal/*": "./src/internal/*.js"
}
}
* 映射公开嵌套的子路径,因为它只是字符串替换语法。
然后,右侧 * 的所有实例都将替换为该值,包括它是否包含任何 / 分隔符。
import featureX from 'es-module-package/features/x';
// 加载 ./node_modules/es-module-package/src/features/x.js
import featureY from 'es-module-package/features/y/y';
// 加载 ./node_modules/es-module-package/src/features/y/y.js
import internalZ from '#internal/z';
// 加载 ./node_modules/es-module-package/src/internal/z.js
这是直接的静态替换,没有对文件扩展名进行任何特殊处理。
在前面的例子中,pkg/features/x.json 将在映射中解析为 ./src/features/x.json.js。
导出的静态可枚举属性由导出模式维护,因为可以通过将右侧目标模式视为针对包内文件列表的 ** glob 来确定包的各个导出。
因为导出目标中禁止 node_modules 路径,所以这个扩展只依赖包本身的文件。
要从模式中排除私有子文件夹,可以使用 null 目标:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js",
"./features/private-internal/*": null
}
}
import featureInternal from 'es-module-package/features/private-internal/m';
// 抛出: ERR_PACKAGE_PATH_NOT_EXPORTED
import featureX from 'es-module-package/features/x';
// 加载 ./node_modules/es-module-package/src/features/x.js
子路径文件夹映射#
中英对照
版本历史
版本变更
v16.0.0
运行时弃用。
v15.1.0
自引用导入的运行时弃用。
v14.13.0, v12.20.0
仅文档弃用。
稳定性: 0 - 弃用: 改为使用子路径模式。
在支持子路径模式之前,尾随 "/" 后缀用于支持文件夹映射:
{
"exports": {
"./features/": "./features/"
}
}
此特性将在未来版本中删除。
而是,使用直接的子路径模式:
{
"exports": {
"./features/*": "./features/*.js"
}
}
模式相对于文件夹导出的好处在于,消费者始终可以导入包,而无需子路径文件扩展名。
导出的语法糖#
中英对照
新增于: v12.11.0
如果 "." 导出是唯一的导出,则 "exports" 字段为这种情况提供了语法糖,即直接的 "exports" 字段值。
如果 "." 导出有回退数组或字符串值,则可以直接将 "exports" 字段设置为此值。
{
"exports": {
".": "./main.js"
}
}
可以写成:
{
"exports": "./main.js"
}
条件导出#
中英对照
版本历史
版本变更
v13.2.0, v12.16.0
新增于: v13.2.0, v12.16.0
v13.7.0, v12.16.0
取消标记条件导出。
条件导出提供了一种根据特定条件映射到不同路径的方法。
CommonJS 和 ES 模块导入都支持它们。
比如,包想要为 require() 和 import 提供不同的 ES 模块导出可以这样写:
// package.json
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-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 而不是原生插件。
条件导出也可以扩展为导出子路径,例如:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./feature": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
定义了一个包,其中 require('pkg/feature') 和 import 'pkg/feature' 可以在 Node.js 和其他 JS 环境之间提供不同的实现。
当使用环境分支时,总是尽可能包含 "default" 条件。
提供 "default" 条件可确保任何未知的 JS 环境都能够使用此通用实现,这有助于避免这些 JS 环境必须伪装成现有环境以支持具有条件导出的包。
出于这个原因,使用 "node" 和 "default" 条件分支通常比使用 "node" 和 "browser" 条件分支更可取。
嵌套的条件#
中英对照
除了直接映射,Node.js 还支持嵌套条件对象。
例如,要定义一个包,它只有双模式入口点用于 Node.js 而不是浏览器:
{
"main": "./main.js",
"exports": {
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
},
"default": "./feature.mjs"
}
}
条件继续按顺序与平面条件匹配。
如果嵌套条件没有任何映射,它将继续检查父条件的剩余条件。
通过这种方式,嵌套条件的行为类似于嵌套的 JavaScript if 语句。
处理用户条件#
中英对照
新增于: v14.9.0, v12.19.0
运行 Node.js 时,可以使用 --conditions 标志添加自定义用户条件:
node --conditions=development main.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 文档创建拉取请求,将新的条件定义添加到此列表中。
在此处列出新条件定义的要求是:
对于所有实现者来说,定义应该是清晰明确的。
为什么需要条件的用例应该清楚地证明。
应该存在足够的现有实现用法。
条件名称不应与另一个条件定义或广泛使用的条件冲突。
条件定义的列表应该为生态系统提供协调效益,否则这是不可能的。
例如,对于特定于公司或特定于应用程序的条件,情况不一定如此。
上述定义可能会在适当的时候移到专门的条件仓库中。
使用名称来引用包#
中英对照
版本历史
版本变更
v13.1.0, v12.16.0
新增于: v13.1.0, v12.16.0
v13.6.0, v12.16.0
使用名称取消标记自引用包。
在一个包中,在包的 package.json "exports" 字段中定义的值可以通过包的名称引用。
例如,假设 package.json 是:
// package.json
{
"name": "a-package",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
}
}
然后该包中的任何模块都可以引用包本身中的导出:
// ./a-module.mjs
import { something } from 'a-package'; // 从 ./main.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'); // 从 ./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