知海

Package Exports

webpackjsorg-main指南-教程

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" } 不同(当同时设置 redgreen 条件时,将使用第一个属性)。

在对象中,如果每个键是一个子路径,则属性(子路径)的顺序不重要。更具体的路径优先于不太具体的路径。

示例:{ "./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 字段优先于其他包入口字段,如 mainmodulebrowser 或自定义字段。

支持情况

特性 支持方
"." 属性 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、esinstall(4)
仅路径的缩写 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。
(仅与 importrequire 组合使用)
webpack、rollup、wmr
esmodules 由支持的工具有条件地设置。 wmr
types 请求来自对类型声明感兴趣的 TypeScript。

(1) importrequire 与引用语法无关,两者都会被设置。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

注意:由于并非所有工具都支持 productiondevelopment,当两者均未设置时,不应做任何假设。

目标环境

以下条件取决于目标环境:

条件 描述 支持方
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) electronworkerworklet 会根据上下文与 nodebrowser 组合出现。

(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-resolveexportConditions 选项
esinstall
wmr

对于自定义条件,推荐使用以下命名模式:

<company-name>:<condition-name>

示例:example-corp:betagoogle: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");

我们更倾向于通过 productiondevelopment 条件进行生产/开发模式的静态检测。

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 请求。
    • 其他绝对或服务器相对请求默认不受支持,但某些工具或环境可能会支持。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。