包的 package.json
中的 exports
字段允许声明在使用 import "package"
或 import "package/sub/path"
等模块请求时应使用哪个模块。它取代了默认实现,默认实现会为 package
返回 main
字段或 index.js
文件,并为 package/sub/path
执行文件系统查找。
当指定 exports
字段时,只有这些模块请求可用。任何其他请求都将导致 ModuleNotFound
错误。
通常,exports
字段应包含一个对象,其中每个属性都指定模块请求的子路径。对于上述示例,可以使用以下属性:import "package"
对应 "."
,import "package/sub/path"
对应 "./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": {
".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
}
}
Webpack 5.94.0+ 现在会抛出错误,因为找不到 non-existent.js
,而之前的行为会解析到 existent.js
。
包作者可以根据环境条件,让模块系统选择一个结果,而不是直接在 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、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 | 请求源自不带模块系统的普通 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 页面作为 script 标签而无需额外预处理的 JavaScript 文件时使用。
为各种优化设置以下条件
条件 | 描述 | 支持者 |
---|---|---|
生产环境 | 在生产环境中。 不应包含任何开发工具。 | 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
:与发布包时的当前规范和阶段 4 提案兼容。polyfill 和转译必须在消费者端处理。
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
条件在用支持 require()
的 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"
}
}
请注意,尽管 dist-bundle.js
使用了 "type": "module"
和 .js
,但此文件并非 ESM 格式。它应使用全局变量,以便可以直接作为 script 标签使用。
当一个包包含两个版本(一个用于开发,一个用于生产)时,这些模式是有意义的。例如,开发版本可以包含用于更好的错误消息或额外警告的附加代码。
{
"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
导出。不同工具对其处理方式不同。只使用命名导出。package.json
中使用 .cjs
或 type: "commonjs"
来明确将源代码标记为 CommonJs。这使得工具可以静态检测是使用了 CommonJs 还是 ESM。这对于只支持 ESM 而不支持 CommonJs 的工具很重要。package.json
的其他包。data:
URL 请求。