模块:包#

模块:包#

包入口点#

在一个包的 package.json 文件中,有两个字段可以定义包的入口点:"main" 和 "exports"。这两个字段都适用于 ES 模块和 CommonJS 模块的入口点。

"main" 字段在所有版本的 Node.js 中都受支持,但其功能有限:它只定义了包的主入口点。

"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"

}

} copy

或者,一个项目可以选择使用导出模式来导出整个文件夹,包括带扩展名和不带扩展名的子路径:

{

"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"

}

} copy

在通过上述方式为任何次要包版本提供向后兼容性后,未来的主版本变更就可以适当地将导出限制为仅暴露特定的功能导出:

{

"name": "my-package",

"exports": {

".": "./lib/index.js",

"./feature/*.js": "./feature/*.js",

"./feature/internal/*": null

}

} copy

主入口点导出#

在编写新包时,建议使用 "exports" 字段:

{

"exports": "./index.js"

} copy

当定义了 "exports" 字段时,包的所有子路径都被封装起来,不再对导入者可用。例如,require('pkg/subpath.js') 会抛出一个 ERR_PACKAGE_PATH_NOT_EXPORTED 错误。

这种导出封装为工具和处理包的 semver 升级提供了关于包接口更可靠的保证。它不是强封装,因为直接 require 包的任何绝对子路径,如 require('/path/to/node_modules/pkg/subpath.js'),仍然会加载 subpath.js。

所有当前支持的 Node.js 版本和现代构建工具都支持 "exports" 字段。对于使用旧版本 Node.js 或相关构建工具的项目,可以通过在 "exports" 旁边包含指向同一模块的 "main" 字段来实现兼容性:

{

"main": "./index.js",

"exports": "./index.js"

} copy

子路径导出#

添加于:v12.7.0

当使用 "exports" 字段时,可以通过将主入口点视为 "." 子路径来定义自定义子路径以及主入口点:

{

"exports": {

".": "./index.js",

"./submodule.js": "./src/submodule.js"

}

} copy

现在,消费者只能导入 "exports" 中定义的子路径:

import submodule from 'es-module-package/submodule.js';

// Loads ./node_modules/es-module-package/src/submodule.js copy

而其他子路径则会报错:

import submodule from 'es-module-package/private-module.js';

// Throws ERR_PACKAGE_PATH_NOT_EXPORTED copy

子路径中的扩展名#

包作者应该在其导出中提供带扩展名(import 'pkg/subpath.js')或不带扩展名(import 'pkg/subpath')的子路径。这确保了每个导出的模块只有一个子路径,从而所有依赖项都导入相同的一致说明符,使包的约定对消费者清晰,并简化了包子路径的自动补全。

传统上,包倾向于使用无扩展名的风格,这具有可读性好和隐藏包内文件真实路径的优点。

随着 import maps 现在为浏览器和其他 JavaScript 运行时中的包解析提供了标准,使用无扩展名的风格可能导致 import map 定义臃肿。明确的文件扩展名可以避免这个问题,它允许 import map 利用包文件夹映射来尽可能多地映射子路径,而不是为每个包子路径导出都创建一个单独的映射条目。这也与在相对和绝对导入说明符中使用完整说明符路径的要求相呼应。

导出目标的路径规则和验证#

在 "exports" 字段中将路径定义为目标时,Node.js 强制执行多项规则以确保安全性、可预测性和适当的封装。理解这些规则对于发布包的作者至关重要。

目标必须是相对 URL#

"exports" 映射中的所有目标路径(与导出键关联的值)必须是以 ./ 开头的相对 URL 字符串。

// package.json

{

"name": "my-package",

"exports": {

".": "./dist/main.js", // Correct

"./feature": "./lib/feature.js", // Correct

// "./origin-relative": "/dist/main.js", // Incorrect: Must start with ./

// "./absolute": "file:///dev/null", // Incorrect: Must start with ./

// "./outside": "../common/util.js" // Incorrect: Must start with ./

}

} copy

这种行为的原因包括:

安全性: 防止从包自身目录之外导出任意文件。

封装性: 确保所有导出的路径都相对于包根目录解析,使包自成一体。

禁止路径遍历或无效段#

导出目标不得解析到包根目录之外的位置。此外,路径段如 .(单点)、..(双点)或 node_modules(及其 URL 编码等效形式)通常不允许出现在 target 字符串中初始 ./ 之后以及替换到目标模式中的任何 subpath 部分中。

// package.json

{

"name": "my-package",

"exports": {

// ".": "./dist/../../elsewhere/file.js", // Invalid: path traversal

// ".": "././dist/main.js", // Invalid: contains "." segment

// ".": "./dist/../dist/main.js", // Invalid: contains ".." segment

// "./utils/./helper.js": "./utils/helper.js" // Key has invalid segment

}

} copy

导出语法糖#

添加于:v12.11.0

如果 "." 导出是唯一的导出,"exports" 字段为此情况提供了语法糖,即直接将值赋给 "exports" 字段。

{

"exports": {

".": "./index.js"

}

} copy

可以写成:

{

"exports": "./index.js"

} copy

子路径导入#

新增于:v14.6.0, v12.19.0

除了 "exports" 字段,还有一个包的 "imports" 字段,用于创建私有映射,这些映射仅适用于包内部的导入说明符。

"imports" 字段中的条目必须始终以 # 开头,以确保它们与外部包说明符相区分。

例如,"imports" 字段可用于为内部模块获得条件导出的好处:

// package.json

{

"imports": {

"#dep": {

"node": "dep-node-native",

"default": "./dep-polyfill.js"

}

},

"dependencies": {

"dep-node-native": "^1.0.0"

}

} copy

这里,import '#dep' 不会解析到外部包 dep-node-native(包括其自身的导出),而是在其他环境中解析到相对于包的本地文件 ./dep-polyfill.js。

与 "exports" 字段不同,"imports" 字段允许映射到外部包。

"imports" 字段的解析规则在其他方面与 "exports" 字段类似。

子路径模式#

历史

版本变更

v16.10.0, v14.19.0

支持在 "imports" 字段中使用模式后缀。

v16.9.0, v14.19.0

支持模式后缀。

v14.13.0, v12.20.0

新增于:v14.13.0, v12.20.0

对于只有少量导出或导入的包,我们建议明确列出每个导出子路径条目。但对于有大量子路径的包,这可能会导致 package.json 臃肿和维护问题。

对于这些用例,可以使用子路径导出模式来代替:

// ./node_modules/es-module-package/package.json

{

"exports": {

"./features/*.js": "./src/features/*.js"

},

"imports": {

"#internal/*.js": "./src/internal/*.js"

}

} copy

* 映射会暴露嵌套的子路径,因为它只是一种字符串替换语法。

右侧的所有 * 实例都将被替换为这个值,即使它包含任何 / 分隔符。

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 copy

这是一种直接的静态匹配和替换,没有对文件扩展名进行任何特殊处理。在映射的两侧都包含 "*.js" 会将暴露的包导出限制为仅 JS 文件。

导出是静态可枚举的这一特性在导出模式中得以保持,因为可以通过将右侧目标模式视为针对包内文件列表的 ** glob 来确定包的单个导出。由于导出目标中禁止使用 node_modules 路径,因此这种扩展仅依赖于包本身的文件。

要从模式中排除私有子文件夹,可以使用 null 目标:

// ./node_modules/es-module-package/package.json

{

"exports": {

"./features/*.js": "./src/features/*.js",

"./features/private-internal/*": null

}

} copy

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 copy

条件导出#

历史

版本变更

v13.7.0, v12.16.0

正式启用条件导出。

v13.2.0, v12.16.0

始于:v13.2.0, v12.16.0

条件导出提供了一种根据特定条件映射到不同路径的方法。它们对 CommonJS 和 ES 模块导入都受支持。

例如,一个希望为 require() 和 import 提供不同 ES 模块导出的包可以这样写:

// package.json

{

"exports": {

"import": "./index-module.js",

"require": "./index-require.cjs"

},

"type": "module"

} copy

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 模块。始终与 "import" 互斥。

"module-sync" - 无论包是通过 import、import() 还是 require() 加载都匹配。格式应为 ES 模块,且其模块图中不包含顶层 await——如果包含,当模块被 require() 时将抛出 ERR_REQUIRE_ASYNC_MODULE。

"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"

}

}

} copy

定义了一个包,其中 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"

}

} copy

条件继续按顺序匹配,就像扁平条件一样。如果一个嵌套条件没有任何映射,它将继续检查父条件的其余条件。这样,嵌套条件的行为类似于嵌套的 JavaScript if 语句。

解析用户条件#

添加于:v14.9.0, v12.19.0

在运行 Node.js 时,可以使用 --conditions 标志添加自定义用户条件:

node --conditions=development index.js copy

这将解析包导入和导出中的 "development" 条件,同时根据情况解析现有的 "node"、"node-addons"、"default"、"import" 和 "require" 条件。

可以通过重复标志设置任意数量的自定义条件。

典型的条件应只包含字母数字字符,必要时可使用 ":"、"-" 或 "=" 作为分隔符。其他任何字符都可能在 Node.js 之外遇到兼容性问题。

在 Node.js 中,条件几乎没有限制,但具体包括:

它们必须至少包含一个字符。

它们不能以 "." 开头,因为它们可能出现在也允许相对路径的地方。

它们不能包含 ",",因为一些 CLI 工具可能会将其解析为逗号分隔的列表。

它们不能是像 "10" 这样的整数属性键,因为这可能对 JS 对象的属性键排序产生意外影响。

社区条件定义#

除了 Node.js 核心中实现的 "import"、"require"、"node"、"module-sync"、"node-addons" 和 "default" 条件之外的条件字符串,默认情况下会被忽略。

其他平台可能会实现其他条件,用户条件可以通过 --conditions / -C 标志在 Node.js 中启用。

由于自定义包条件需要明确的定义以确保正确使用,下面提供了一个常见的已知包条件及其严格定义的列表,以协助生态系统的协调。

"types" - 可被类型系统用于解析给定导出的类型定义文件。此条件应始终放在首位。

"browser" - 任何 Web 浏览器环境。

"development" - 可用于定义仅限开发环境的入口点,例如在开发模式下运行时提供额外的调试上下文,如更好的错误消息。必须始终与 "production" 互斥。

"production" - 可用于定义生产环境的入口点。必须始终与 "development" 互斥。

对于其他运行时,平台特定的条件键定义由 WinterCG 在 运行时键(Runtime Keys)提案规范中维护。

可以通过向此部分的 Node.js 文档创建一个拉取请求来向此列表添加新的条件定义。在此处列出新条件定义的要求是:

定义对于所有实现者来说都应该是清晰无歧义的。

需要该条件的用例应有清晰的理由。

应有足够多的现有实现使用案例。

条件名称不应与另一个条件定义或广泛使用的条件冲突。

列出条件定义应为生态系统带来协调上的好处,而这种好处在其他情况下是无法实现的。例如,对于公司特定或应用特定的条件,情况可能并非如此。

该条件应使得 Node.js 用户期望它出现在 Node.js 核心文档中。"types" 条件就是一个很好的例子:它不属于 运行时键 提案,但很适合放在 Node.js 文档中。

上述定义可能会在适当的时候移至一个专门的条件注册表。

使用包名自引用包#

历史

版本变更

v13.6.0, v12.16.0

正式启用使用包名自引用包的功能。

v13.1.0, v12.16.0

新增于:v13.1.0, v12.16.0

在一个包内部,可以通过包的名称引用其 package.json "exports" 字段中定义的值。例如,假设 package.json 如下:

// package.json

{

"name": "a-package",

"exports": {

".": "./index.mjs",

"./foo.js": "./foo.js"

}

} copy

那么,该包中的任何模块 都可以引用包自身的导出:

// ./a-module.mjs

import { something } from 'a-package'; // Imports "something" from ./index.mjs. copy

只有当 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'; copy

在使用 require 时,无论是在 ES 模块中还是在 CommonJS 模块中,自引用都可用。例如,这段代码也能工作:

// ./a-module.js

const { something } = require('a-package/foo.js'); // Loads from ./foo.js. copy

最后,自引用也适用于带作用域的包(scoped packages)。例如,这段代码也能工作:

// package.json

{

"name": "@my/package",

"exports": "./index.js"

} copy

// ./index.js

module.exports = 42; copy

// ./other.js

console.log(require('@my/package')); copy

$ node other.js

42 copy

相关推荐

解决uTorrent下载不工作的问题:故障排除与解决方案
《华严经》有多少版本?
在哪个应用商店能下载365

《华严经》有多少版本?

📅 01-22 👍 158
北京八大处整形医院怎么样?专家推荐+口碑案例分享
在哪个应用商店能下载365

北京八大处整形医院怎么样?专家推荐+口碑案例分享

📅 10-27 👍 367
阿里云服务器public key如何获取
在哪个应用商店能下载365

阿里云服务器public key如何获取

📅 08-06 👍 657
侯門深似海
365bet返水多少

侯門深似海

📅 08-17 👍 554
在上下文、翻译记忆库中将“陌
在哪个应用商店能下载365

在上下文、翻译记忆库中将“陌"翻译成 中文

📅 07-09 👍 238