Package Exports
Package Exports
包的 package.json 中的 exports 字段允许声明当使用 import "package" 或 import "package/sub/path" 这类模块请求时应使用哪个模块。它取代了默认实现——默认实现会返回 main 字段或 index.js 文件作为 "package" 的入口,以及通过文件系统查找 "package/sub/path"。
当指定了 exports 字段时,只有这些模块请求可用。任何其他请求都将导致 ModuleNotFound Error(模块未找到错误)。
通用语法
通常,exports 字段应包含一个对象,每个属性指定模块请求的一个子路径。对于上面的示例,可以使用以下属性:"." 用于 import "package","./sub/path" 用于 import "package/sub/path"。以 / 结尾的属性会将带有此前缀的请求转发到旧的文件系统查找算法。对于以 * 结尾的属性,* 可以是任何值,属性值中的任何 * 都会被替换为所取的值。
示例:
json
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
| 模块请求 | 结果 |
|---|---|
package |
.../package/main.js |
package/sub/path |
.../package/secondary.js |
package/prefix/some/file.js |
.../package/directory/some/file.js |
package/prefix/deep/file.js |
.../package/other-directory/file.js |
package/other-prefix/deep/file.js |
.../package/yet-another/deep/file/deep/file.js |
package/main.js |
错误 |
备选方案
包作者可以提供一组结果,而不是单个结果。在这种情况下,将按顺序尝试该列表,并使用第一个有效的结果。
注意:只会使用第一个有效的结果,而不是所有有效的结果。
示例:
json
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
在这里,package/things/apple 可能位于 .../package/good-things/apple 或 .../package/bad-things/apple。
警告: 自 webpack 5.94.0 版本起,webpack 的行为已更新为与 Node.js 的行为一致。现在它选择第一个有效路径,而不尝试进一步解析,并且如果路径无法解析,会抛出错误。
例如,对于以下配置:
json
{
"exports": {
".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
}
}
Webpack 5.94.0+ 将抛出错误,因为 non-existent.js 未找到,而之前的行为会解析到 existent.js。
条件语法
包作者可以不直接在 exports 字段中提供结果,而是让模块系统根据环境条件选择一个结果。
在这种情况下,应使用将条件映射到结果的对象。条件按对象顺序进行尝试。包含无效结果的条件将被跳过。条件可以嵌套以形成逻辑 AND。对象中的最后一个条件可以是特殊的 "default" 条件,它始终会匹配。
示例:
json
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
这可以转换为类似如下的逻辑:
ts
if (red && valid("./stop.js")) return "./stop.js";
if (yellow && valid("./stop.js")) return "./stop.js";
if (green) {
if (free && valid("./drive.js")) return "./drive.js";
if (valid("./wait.js")) return "./wait.js";
}
if (valid("./drive-carefully.js")) return "./drive-carefully.js";
throw new ModuleNotFoundError();
可用的条件取决于所使用的模块系统和工具。
缩写
当只需要支持对包的单一路径(".")时,可以省略 { ".": ... } 对象嵌套:
json
{
"exports": "./index.mjs"
}
json
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
关于顺序的说明
在对象中,如果每个键是一个条件,则属性的顺序很重要。条件将按照它们被指定的顺序处理。
示例:{ "red": "./stop.js", "green": "./drive.js" } 与 { "green": "./drive.js", "red": "./stop.js" } 不同(当同时设置 red 和 green 条件时,将使用第一个属性)。
在对象中,如果每个键是一个子路径,则属性(子路径)的顺序不重要。更具体的路径优先于不太具体的路径。
示例:{ "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" } 等于 { "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" }(顺序始终为:./a/b/c > ./a/b/ > ./a/)。
exports 字段优先于其他包入口字段,如 main、module、browser 或自定义字段。
支持情况
| 特性 | 支持方 |
|---|---|
"." 属性 |
Node.js、webpack、rollup、esinstall、wmr |
| 普通属性 | Node.js、webpack、rollup、esinstall、wmr |
以 / 结尾的属性 |
|
以 * 结尾的属性 |
Node.js、webpack、rollup、esinstall |
| 备选方案 | Node.js、webpack、rollup、 |
| 仅路径的缩写 | Node.js、webpack、rollup、esinstall、wmr |
| 仅条件的缩写 | Node.js、webpack、rollup、esinstall、wmr |
| 条件语法 | Node.js、webpack、rollup、esinstall、wmr |
| 嵌套条件语法 | Node.js、webpack、rollup、wmr(5) |
| 条件顺序 | Node.js、webpack、rollup、wmr(6) |
"default" 条件 |
Node.js、webpack、rollup、esinstall、wmr |
| 路径顺序 | Node.js、webpack、rollup |
| 未映射时抛出错误 | Node.js、webpack、rollup、esinstall、wmr(7) |
| 混合条件和路径时抛出错误 | Node.js、webpack、rollup |
(1) 在 Node.js 17 中移除,请使用 * 代替。
(2) "./" 作为键会被有意忽略。
(3) 属性值会被忽略,属性键被用作目标。实际上只允许键和值相同的映射。
(4) 该语法受支持,但始终使用第一个条目,这使其无法用于任何实际用例。
(5) 回退到备选兄弟父条件的处理方式不正确。
(6) 对于 require 条件,对象顺序处理不正确。这是有意为之,因为 wmr 不区分引用语法。
(7) 当使用 "exports": "./file.js" 缩写时,任何请求(例如 package/not-existing)都会解析到该文件。当不使用缩写时,直接文件访问(例如 package/file.js)不会导致错误。
条件
引用语法
根据引用模块所使用的语法,会设置以下条件之一:
| 条件 | 描述 | 支持方 |
|---|---|---|
import |
请求来自 ESM 语法或类似语法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
require |
请求来自 CommonJS/AMD 语法或类似语法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
style |
请求来自样式表引用。 | |
sass |
请求来自 sass 样式表引用。 | |
asset |
请求来自资源引用。 | |
script |
请求来自没有模块系统的普通脚本标签。 |
以下条件可能也会被额外设置:
| 条件 | 描述 | 支持方 |
|---|---|---|
module |
所有允许引用 JavaScript 的模块语法都支持 ESM。 (仅与 import 或 require 组合使用) |
webpack、rollup、wmr |
esmodules |
由支持的工具有条件地设置。 | wmr |
types |
请求来自对类型声明感兴趣的 TypeScript。 |
(1) import 和 require 与引用语法无关,两者都会被设置。require 的优先级始终较低。
import
以下语法会设置 import 条件:
- ESM 中的 ESM
import声明 - JS
import()表达式 - HTML 中的
<script type="module"> - HTML 中的
<link rel="preload/prefetch"> - JS
new Worker(..., { type: "module" }) - WASM
import段 - ESM HMR(webpack)
import.hot.accept/decline([...]) - JS
Worklet.addModule - 使用 JavaScript 作为入口点
require
以下语法会设置 require 条件:
- CommonJS
require(...) - AMD
define() - AMD
require([...]) - CommonJS
require.resolve() - CommonJS(webpack)
require.ensure([...]) - CommonJS(webpack)
require.context - CommonJS HMR(webpack)
module.hot.accept/decline([...]) - HTML
<script src="...">
style
以下语法会设置 style 条件:
- CSS
@import - HTML
<link rel="stylesheet">
asset
以下语法会设置 asset 条件:
- CSS
url() - ESM
new URL(..., import.meta.url) - HTML
<img src="...">
script
以下语法会设置 script 条件:
- HTML
<script src="...">
script 只应在不支持模块系统时设置。
当脚本被支持 CommonJS 的系统预处理时,应设置 require 而不是 script。
当需要查找一个可以直接作为脚本标签注入 HTML 页面而无需额外预处理的 JavaScript 文件时,应使用此条件。
优化
以下条件用于各种优化:
| 条件 | 描述 | 支持方 |
|---|---|---|
production |
在生产环境中。 不应包含开发工具代码。 |
webpack |
development |
在开发环境中。 应包含开发工具代码。 |
webpack |
注意:由于并非所有工具都支持 production 和 development,当两者均未设置时,不应做任何假设。
目标环境
以下条件取决于目标环境:
| 条件 | 描述 | 支持方 |
|---|---|---|
browser |
代码将在浏览器中运行。 | webpack、esinstall、wmr |
electron |
代码将在 Electron 中运行。(1) | webpack |
worker |
代码将在(Web)Worker 中运行。(1) | webpack |
worklet |
代码将在 Worklet 中运行。(1) | |
node |
代码将在 Node.js 中运行。 | Node.js、webpack、wmr(2) |
deno |
代码将在 Deno 中运行。 | |
react-native |
代码将在 react-native 中运行。 |
(1) electron、worker 和 worklet 会根据上下文与 node 或 browser 组合出现。
(2) 这是针对浏览器目标环境设置的。
由于每种环境都有多个版本,应遵循以下准则:
node:请参阅engines字段以了解兼容性。browser:与 Web 标准以及发布包时的第 4 阶段提案兼容。Polyfill 或转译必须在消费者侧处理。- 无法 polyfill 或转译的特性应谨慎使用,因为这会限制可能的用途。
deno:待定react-native:待定
条件:预处理器和运行时
以下条件取决于哪个工具预处理源代码:
| 条件 | 描述 | 支持方 |
|---|---|---|
webpack |
由 webpack 处理。 | webpack |
遗憾的是,没有针对 Node.js 运行时的 node-js 条件。这将有助于为 Node.js 创建例外。
条件:自定义
以下工具支持自定义条件:
| 工具 | 支持 | 备注 |
|---|---|---|
| Node.js | 是 | 使用 --conditions 命令行参数。 |
| webpack | 是 | 使用 resolve.conditionNames 配置选项。 |
| rollup | 是 | 使用 @rollup/plugin-node-resolve 的 exportConditions 选项 |
| esinstall | 否 | |
| wmr | 否 |
对于自定义条件,推荐使用以下命名模式:
<company-name>:<condition-name>
示例:example-corp:beta、google:internal。
常见模式
所有模式都以单个 "." 入口进行说明,但通过为每个入口重复该模式,也可以扩展到多个入口。
这些模式应作为指南而非严格规则集来使用。可以根据具体包进行调整。
这些模式基于以下目标和假设:
- 包会逐渐腐化。
- 我们假设在某些时候包不再被维护,但仍然是使用的。
exports的编写应使用回退机制以应对未知的未来情况。default条件可用于此目的。- 由于未来是未知的,我们假设环境类似于浏览器,模块系统类似于 ESM。
- 并非所有工具都支持所有条件。
- 应使用回退机制来处理这些情况。
- 我们假设以下回退机制在一般情况下是合理的:
- ESM > CommonJS
- 生产 > 开发
- 浏览器 > node.js
根据包的意图,也许其他方案更有意义,此时应调整模式以适应需求。例如:对于命令行工具来说,类浏览器的未来和回退没有太大意义,在这种情况下,应改为使用类 Node.js 的环境和回退。
对于复杂用例,需要通过嵌套这些条件来组合多种模式。
与目标环境无关的包
这些模式适用于不使用环境特定 API 的包。
仅提供 ESM 版本
json
{
"type": "module",
"exports": "./index.js"
}
注意:仅提供 ESM 版本在 Node.js 中会受到限制。这样的包只能在使用 import 时在 Node.js >= 14 中运行。它不能与 require() 一起使用。
提供 CommonJS 和 ESM 版本(无状态)
json
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
大多数工具会获得 ESM 版本。Node.js 是例外。当使用 require() 时,它会获得 CommonJS 版本。这将导致在使用 require() 和 import 引用该包时出现两个实例,但由于包没有状态,这不会造成影响。
module 条件被用作优化,当使用支持 ESM 的预处理工具处理针对 Node.js 的代码时(例如,为 Node.js 打包的 bundler),可以跳过该例外。从技术上讲这是可选的,但如果不使用,bundler 将两次包含包源代码。
如果可以隔离包的状态到 JSON 文件中,也可以使用无状态模式。JSON 可以从 CommonJS 和 ESM 中消费,而不会用其他模块系统污染图。
请注意,这里的无状态也意味着类实例不能使用 instanceof 进行测试,因为可能存在两个不同的类。
提供 CommonJS 和 ESM 版本(有状态)
json
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
js
// wrapper.js
import cjs from "./index.cjs";
export const A = cjs.A;
export const B = cjs.B;
在有状态的包中,我们必须确保包永远不会被实例化两次。
这对大多数工具来说不是问题,但 Node.js 再次成为例外。对于 Node.js,我们始终使用 CommonJS 版本,并通过 ESM wrapper 在 ESM 中暴露命名导出。
我们再次使用 module 条件作为优化。
仅提供 CommonJS 版本
json
{
"type": "commonjs",
"exports": "./index.js"
}
提供 "type": "commonjs" 有助于静态检测 CommonJS 文件。
提供用于浏览器直接消费的打包脚本版本
json
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
注意,尽管使用了 "type": "module" 和 .js 扩展名,dist-bundle.js 文件并不是 ESM 格式。它应使用全局变量以允许作为脚本标签直接消费。
提供开发工具或生产优化
这些模式适用于包含两个版本(一个用于开发,一个用于生产)的包。例如,开发版本可以包含用于更好错误消息或额外警告的附加代码。
无 Node.js 运行时检测
json
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
当支持 development 条件时,我们使用为开发增强的版本。否则,在生产环境或模式未知时,我们使用优化版本。
有 Node.js 运行时检测
json
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"node": "./wrapper-process-env.cjs",
"default": "./index-optimized.js"
}
}
wrapper-process-env.cjs
js
module.exports =
process.env.NODE_ENV !== "development"
? require("./index-optimized.cjs")
: require("./index-with-devtools.cjs");
我们更倾向于通过 production 或 development 条件进行生产/开发模式的静态检测。
Node.js 允许在运行时通过 process.env.NODE_ENV 检测生产/开发模式,因此我们将其作为 Node.js 中的回退方案。由于 ESM 无法进行同步条件导入,且我们不希望加载包两次,因此必须使用 CommonJS 进行运行时检测。
当无法检测模式时,我们回退到生产版本。
根据目标环境提供不同版本
应选择一个有意义的回退环境来支持未来的环境。通常应假设为类浏览器环境。
提供 Node.js、WebWorker 和浏览器版本
json
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
提供 Node.js、浏览器和 Electron 版本
json
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
组合模式
示例 1
这是一个包示例,它包含生产和开发使用的优化,通过 process.env 进行运行时检测,同时提供 CommonJS 和 ESM 版本:
json
{
"type": "module",
"exports": {
"node": {
"development": {
"module": "./index-with-devtools.js",
"import": "./wrapper-with-devtools.js",
"require": "./index-with-devtools.cjs"
},
"production": {
"module": "./index-optimized.js",
"import": "./wrapper-optimized.js",
"require": "./index-optimized.cjs"
},
"default": "./wrapper-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
示例 2
这是一个包示例,它支持 Node.js、浏览器和 Electron,包含生产和开发使用的优化,通过 process.env 进行运行时检测,同时提供 CommonJS 和 ESM 版本:
json
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
看起来复杂,是的。我们已经能够通过一个假设来降低一些复杂性:只有 node 需要 CommonJS 版本,并且可以使用 process.env 检测生产/开发模式。
指南
- 避免使用
default导出。它在不同工具之间处理方式不同。只使用命名导出。 - 永远不要为不同条件提供不同的 API 或语义。
- 使用 ESM 编写源代码,并通过 babel、typescript 或类似工具转译为 CJS。
- 使用
.cjs扩展名或package.json中的type: "commonjs",以明确将源代码标记为 CommonJS。这使得工具可以静态检测使用的是 CommonJS 还是 ESM。这对于只支持 ESM 而不支持 CommonJS 的工具非常重要。 - 包中使用的 ESM 支持以下类型的请求:
- 模块请求,指向带有 package.json 的其他包。
- 相对请求,指向包内的其他文件。
- 它们不能指向包外的文件。
data:URL 请求。- 其他绝对或服务器相对请求默认不受支持,但某些工具或环境可能会支持。
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
