包的 package.json
中的 exports
字段允许声明在使用模块请求(如 import "package"
或 import "package/sub/path"
)时应使用哪个模块。它取代了默认实现,该实现返回 main
字段(分别为 index.js
文件)用于 "package"
,以及用于 "package/sub/path"
的文件系统查找。
当指定 exports
字段时,只有这些模块请求可用。任何其他请求都将导致 ModuleNotFound 错误。
通常,exports
字段应包含一个对象,其中每个属性指定模块请求的子路径。对于上面的示例,可以使用以下属性:"."
用于 import "package"
,"./sub/path"
用于 import "package/sub/path"
。以 /
结尾的属性将把具有此前缀的请求转发到旧的文件系统查找算法。对于以 *
结尾的属性,*
可以取任何值,并且属性值中的任何 *
都将被取的值替换。
一个例子
{
"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 | 错误 |
包作者可以提供结果列表,而不是提供单个结果。在这种情况下,将按顺序尝试此列表,并将使用第一个有效结果。
注意:只使用第一个有效结果,而不是所有有效结果。
示例
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
这里 package/things/apple
可能在 .../package/good-things/apple
或 .../package/bad-things/apple
中找到。
包作者可以不直接在 exports
字段中提供结果,而是让模块系统根据环境条件选择一个结果。
在这种情况下,应该使用一个对象,将条件映射到结果。条件将按照对象中的顺序进行尝试。包含无效结果的条件将被跳过。条件可以嵌套以创建逻辑 AND。对象中的最后一个条件可能是特殊的 "default"
条件,该条件始终匹配。
示例
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
这可以翻译成类似以下内容
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();
可用的条件根据使用的模块系统和工具而有所不同。
当包中只应该支持单个入口("."
)时,可以省略 { ".": ... }
对象嵌套。
{
"exports": "./index.mjs"
}
{
"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(1)、webpack、rollup、esinstall(2)、wmr(3) |
以 * 结尾的属性 | 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 中已弃用,应优先使用 *
。
(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
声明import()
表达式<script type="module">
<link rel="preload/prefetch">
new Worker(..., { type: "module" })
import
部分import.hot.accept/decline([...])
Worklet.addModule
以下语法将设置 require
条件
require(...)
define()
require([...])
require.resolve()
require.ensure([...])
require.context
module.hot.accept/decline([...])
<script src="...">
以下语法将设置 style
条件
@import
<link rel="stylesheet">
以下语法将设置 asset
条件
url()
new URL(..., import.meta.url)
<img src="...">
以下语法将设置 script
条件
<script src="...">
script
仅在不支持模块系统时设置。当脚本由支持 CommonJs 的系统预处理时,应设置 require
。
当查找可以作为脚本标签注入 HTML 页面而无需额外预处理的 JavaScript 文件时,应使用此条件。
以下条件用于各种优化
条件 | 描述 | 支持 |
---|---|---|
生产 | 在生产环境中。 不应包含任何调试工具。 | webpack |
开发 | 在开发环境中。 应包含调试工具。 | webpack |
注意:由于 production
和 development
不被所有人支持,因此在没有设置任何这些条件时,不应做出任何假设。
以下条件根据目标环境设置
条件 | 描述 | 支持 |
---|---|---|
浏览器 | 代码将在浏览器中运行。 | 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
:与发布包时的当前规范和第 4 阶段提案兼容。填充或转译必须由消费者端处理。deno
:待定react-native
:待定以下条件根据哪个工具预处理源代码而设置。
条件 | 描述 | 支持 |
---|---|---|
webpack | 由 webpack 处理。 | webpack |
遗憾的是,Node.js 作为运行时没有 node-js
条件。这将简化创建 Node.js 异常。
以下工具支持自定义条件
工具 | 支持 | 备注 |
---|---|---|
Node.js | 是 | 使用 --conditions CLI 参数。 |
webpack | 是 | 使用 resolve.conditionNames 配置选项。 |
rollup | 是 | 使用 @rollup/plugin-node-resolve 的 exportConditions 选项 |
esinstall | 否 | |
wmr | 否 |
对于自定义条件,建议使用以下命名方案
<公司名称>:<条件名称>
示例:example-corp:beta
、google:internal
。
所有模式都用包中的单个 "."
条目进行解释,但也可以通过为每个条目重复模式从多个条目扩展。
这些模式应作为指南使用,而不是严格的规则集。它们可以适应各个包。
这些模式基于以下目标/假设列表
exports
应该使用回退来处理未知的未来情况。default
条件可以用于此目的。根据包的意图,可能其他一些东西更有意义,在这种情况下,应该采用这些模式。例如:对于命令行工具,浏览器式的未来和回退没有太大意义,在这种情况下,应该使用 node.js 式的环境和回退。
对于复杂的用例,需要通过嵌套这些条件来组合多种模式。
这些模式对于不使用环境特定 API 的包是有意义的。
{
"type": "module",
"exports": "./index.js"
}
注意:仅提供 ESM 会对 node.js 造成限制。这样的包只能在 Node.js >= 14 中使用,并且仅在使用 import
时才可以使用。它不能与 require()
一起使用。
{
"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 为目标的代码时。对于这样的工具,异常会被跳过。这在技术上是可选的,但捆绑器否则会包含两次包源代码。
如果能够将包状态隔离在 JSON 文件中,也可以使用无状态模式。JSON 可以从 CommonJs 和 ESM 中使用,而不会用其他模块系统污染图。
请注意,这里无状态也意味着类实例不会使用instanceof
进行测试,因为由于双模块实例化,可能存在两个不同的类。
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
// wrapper.js
import cjs from './index.cjs';
export const A = cjs.A;
export const B = cjs.B;
在有状态的包中,我们必须确保包永远不会被实例化两次。
这对大多数工具来说不是问题,但 Node.js 又是这里的例外。对于 Node.js,我们始终使用 CommonJs 版本,并在 ESM 中使用 ESM 包装器公开命名导出。
我们再次使用module
条件作为优化。
{
"type": "commonjs",
"exports": "./index.js"
}
提供"type": "commonjs"
有助于静态检测 CommonJs 文件。
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
请注意,尽管使用"type": "module"
和.js
用于dist-bundle.js
,但此文件并非 ESM 格式。它应该使用全局变量以允许作为脚本标签直接使用。
当一个包包含两个版本时,这些模式是有意义的,一个用于开发,一个用于生产。例如,开发版本可以包含用于更好的错误消息或额外警告的额外代码。
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
当支持development
条件时,我们使用为开发增强的版本。否则,在生产环境中或模式未知时,我们使用优化后的版本。
{
"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
if (process.env.NODE_ENV !== 'development') {
module.exports = require('./index-optimized.cjs');
} else {
module.exports = require('./index-with-devtools.cjs');
}
我们更喜欢通过production
或development
条件静态检测生产/开发模式。
Node.js 允许通过process.env.NODE_ENV
在运行时检测生产/开发模式,因此我们在 Node.js 中将其用作后备。同步条件导入 ESM 是不可能的,我们也不想加载两次包,因此我们必须使用 CommonJs 进行运行时检测。
当无法检测模式时,我们会回退到生产版本。
应选择一个合理的回退环境,以便该包能够支持未来的环境。一般来说,应该假设一个类似浏览器的环境。
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
这是一个示例,该示例展示了一个包,该包针对生产和开发使用进行了优化,并使用运行时检测 process.env
,同时还提供 CommonJs 和 ESM 版本。
{
"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"
}
}
这是一个示例,该示例展示了一个包,该包支持 Node.js、浏览器和 Electron,针对生产和开发使用进行了优化,并使用运行时检测 process.env
,同时还提供 CommonJs 和 ESM 版本。
{
"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
导出。它在不同的工具之间处理方式不同。只使用命名导出。.cjs
或 type: "commonjs"
来明确标记源代码为 CommonJs。这使得工具可以静态地检测到是否使用 CommonJs 或 ESM。这对只支持 ESM 而不支持 CommonJs 的工具很重要。data:
URL 请求受支持。