Webpack Package exports

2023-06-03 14:21 更新

package.json​ 中的 ​exports​ 字段允许声明在使用模块请求(如​import "package"​ 或 ​import "package/sub/path"​)时应使用哪个模块。它替代了 default 实现,该实现返回 "package" 的 ​main​ 字段或index.js文件,以及对​"package/sub/path"​进行文件系统查找。

当指定​ exports​ 字段时,只有这些模块请求可用。任何其他请求将导致ModuleNotFound错误。

General syntax

一般情况下,​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"
  }
}
Module request Result
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 Error

Alternatives

与提供单个结果不同,包作者可以提供一系列结果。在这种情况下,将按顺序尝试此列表,并使用第一个有效结果。

注意:只使用第一个有效结果,而不是所有有效结果。

举例:

{
  "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();

可用的条件取决于所使用的模块系统和工具。

Abbreviation

当仅支持对包中的单个条目(​"."​)的访问时,可以省略 ​{ ".": ... }​ 对象嵌套:

{
  "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​或自定义字段。

Support

Feature Supported by
"." property Node.js, webpack, rollup, esinstall, wmr
normal property Node.js, webpack, rollup, esinstall, wmr
property ending with / Node.js(1), webpack, rollup, esinstall(2), wmr(3)
property ending with * Node.js, webpack, rollup, esinstall
Alternatives Node.js, webpack, rollup, esinstall(4)
Abbreviation only path Node.js, webpack, rollup, esinstall, wmr
Abbreviation only conditions Node.js, webpack, rollup, esinstall, wmr
Conditional syntax Node.js, webpack, rollup, esinstall, wmr
Nested conditional syntax Node.js, webpack, rollup, wmr(5)
Conditions Order Node.js, webpack, rollup, wmr(6)
"default" condition Node.js, webpack, rollup, esinstall, wmr
Path Order Node.js, webpack, rollup
Error when not mapped Node.js, webpack, rollup, esinstall, wmr(7)
Error when mixing conditions and paths Node.js, webpack, rollup

(1) 在Node.js中已弃用,应优先使用​*​。

(2) ​"./"​被有意忽略作为键。

(3) 属性值被忽略,属性键被用作目标。实际上,只允许键和值相同的映射。

(4) 语法是支持的,但始终使用第一个条目,这使其在任何实际用例中都无法使用。

(5) 回退到替代的同级父级条件处理错误。

(6) 对于​require​条件对象,对象顺序处理不正确。这是有意的,因为wmr不区分引用语法。

(7) 当使用​"exports"​: ​"./file.js"​缩写时,任何请求,例如​package/not-existing​ 将解析到该文件。当不使用缩写时,直接访问文件,例如 ​package/file.js​ 将不会导致错误。

条件

引用语法

根据用于引用模块的语法之一设置以下条件:

Condition Description Supported by
import Request is issued from ESM syntax or similar. Node.js, webpack, rollup, esinstall(1), wmr(1)
require Request is issued from CommonJs/AMD syntax or similar. Node.js, webpack, rollup, esinstall(1), wmr(1)
style Request is issued from a stylesheet reference.
sass Request is issued from a sass stylesheet reference.
asset Request is issued from a asset reference.
script Request is issued from a normal script tag without module system.

这些条件也可以额外设置:

Condition Description Supported by
module All module syntax that allows to reference javascript supports ESM.
(only combined with import or require)
webpack, rollup, wmr
esmodules Always set by supported tools. wmr
types Request is issued from typescript that is interested in type declarations.

(1)​import​ 和 ​require​ 都是独立于引用语法设置的。 ​require​ 始终具有较低的优先级。

import

以下语法将设置导入条件:

  • ESM ESM 中的 import 申报

  • JS ​import()​ 表达式
  • HTML ​<script type="module">​ 在 HTML 中
  • HTML ​<link rel="preload/prefetch">​ 在 HTML 中
  • 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

以下语法将设置样式条件:

  • CSS @import
  • HTML ​<link rel="stylesheet">

asset

The following syntax will set the asset condition:

  • CSS ​url()
  • ESM ​new URL(..., import.meta.url)
  • HTML ​<img src="...">

script

以下语法将设置脚本条件:

  • HTML ​<script src="...">

仅当不支持模块系统时才应设置 script 。当 script 由支持 CommonJs 的系统预处理时,它应该改为设置 ​require​ 。

在寻找可以作为脚本标记注入 HTML 页面而无需额外预处理的 javascript 文件时,应使用此条件。

Optimizations

The following conditions are set for various optimizations:

Condition Description Supported by
production 在生产环境中,不应包括开发工具 webpack
development 在开发环境中,应该包括开发工具 webpack

Note: 

由于 production 和 development 不是每个人都支持的,所以当这些都没有设置时,不要做任何假设。

Target environment

根据目标环境设置以下条件:

Condition Description Supported by
browser 代码将在浏览器中运行。 webpack, esinstall, wmr
electron代码将在electron中运行。 webpack
worker代码将在worker中运行。 webpack
worklet 代码将在worklet中运行。 -
node 代码将在node中运行。 Node.js, webpack, wmr
deno 代码将在deno中运行。 -
react-native代码将在react-native中运行。 -

(1)根据上下文,​electron​、​worker​和​worklet​可以与 node 和 browser结合使用。

(2)这是为浏览器目标环境设置的。

由于每个环境都有多个版本,因此适用以下指导原则:

  • node:请参阅引擎字段以了解兼容性。

浏览器:在发布包时兼容当前的规范和第4阶段建议。Polyfilling职责。编译必须在消费者端处理。不可能填充或转译的特性应该小心使用,因为它限制了可能的使用。

  • deno​: TBD
  • react-native​: TBD

Conditions: Preprocessor and runtimes

根据对源代码进行预处理的工具设置以下条件。

Condition Description Supported by
webpack 由webpack处理 webpack

遗憾的是,Node.js运行时没有 ​node-js​ 条件。这将简化为Node.js创建异常。

Conditions: Custom

以下工具支持自定义条件:

Tool Supported Notes
Node.js yes Use --conditions CLI argument.
webpack yes Use resolve.conditionNames configuration option.
rollup yes Use exportConditions option for @rollup/plugin-node-resolve
esinstall no
wmr no

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

<company-name>:<condition-name>

例如:​example-corp:beta​, ​google:internal​。

Common patterns

所有模式都用包中的单个​"."​条目来解释,但是它们也可以从多个条目扩展,通过为每个条目重复模式。

这些模式应该用作指南,而不是严格的规则集。它们可以适应不同的软件包。

这些模式基于以下目标/假设列表:

  • 我们假设在某些时候不再维护包,但是它们继续被使用。应该编写导出,以便为未来未知的情况使用回退。可以使用 default 条件。由于未来是未知的,我们假设一个类似浏览器的环境和类似ESM的模块系统。
  • 并非所有工具都支持所有条件。应该使用回退来处理这些情况。我们假设下面的回退通常是有意义的:
  1. ESM > CommonJs
  2. Production > Development
  3. Browser > node.js

根据package的意图,可能有些其他的东西是有意义的,在这种情况下应该采用模式。示例:对于命令行工具来说,类似浏览器的未来和回退并没有多大意义,在这种情况下,应该使用类似node.js的环境和回退。

对于复杂的用例,需要通过嵌套这些条件来组合多个模式。

Target environment independent packages

这些模式对于不使用特定于环境的api的包是有意义的。

Providing only an ESM version

{
  "type": "module",
  "exports": "./index.js"
}

Note: 

只提供ESM对node.js来说是有限制的。这样的包只能在Node.js >= 14中工作,并且只能在使用 ​import​ 时工作。它不能与 ​require()​ 一起工作。

Providing CommonJs and ESM version (stateless)

{
  "type": "module",
  "exports": {
    "node": {
      "module": "./index.js",
      "require": "./index.cjs"
    },
    "default": "./index.js"
  }
}

大多数工具都是ESM版本。Node.js在这里是个例外。当使用 ​require()​ 时,它得到一个CommonJs版本。当使用 ​require()​ 和 ​import​ 引用包时,这将导致这些包的两个实例,但这不会造成损害,因为包没有状态。

当使用支持ESM的 ​require()​ 工具预处理面向节点的代码时, module 条件被用作优化(就像绑定器一样,在为Node.js绑定时)。对于这样的工具,将跳过异常。这在技术上是可选的,但如果不这样做,打包器将包含两次包源代码。

如果能够在JSON文件中隔离包状态,也可以使用无状态模式。JSON可以从CommonJs和ESM中使用,而不会用其他模块系统污染图形。

请注意,这里的无状态还意味着不使用instanceof测试类实例,因为由于双模块实例化,可能存在两个不同的类。

Providing CommonJs and ESM version (stateful)

{
  "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 条件作为优化。

Providing only a CommonJs version

{
  "type": "commonjs",
  "exports": "./index.js"
}

提供​"type": "commonjs"​有助于静态检测commonjs文件。

Providing a bundled script version for direct browser consumption

{
  "type": "module",
  "exports": {
    "script": "./dist-bundle.js",
    "default": "./index.js"
  }
}
Note:
尽管在 ​dist-bundle.js​ 中使用了 ​"type": "module"​和​.js​,但这个文件不是ESM格式的。它应该使用全局变量来允许直接使用脚本标记。

Providing devtools or production optimizations

当一个package包含两个版本,一个用于开发,一个用于生产时,这些模式是有意义的。例如,开发版本可以包含额外的代码,以提供更好的错误信息或额外的警告。

Without Node.js runtime detection

{
  "type": "module",
  "exports": {
    "development": "./index-with-devtools.js",
    "default": "./index-optimized.js"
  }
}

当开发条件得到支持时,我们使用版本增强进行开发。否则,在生产中或模式未知时,我们使用优化版本。

With Node.js runtime detection

{
  "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来进行运行时检测。

当无法检测到模式时,我们会退回到生产版本。

Providing different versions depending on target environment

应该选择一个对包支持未来环境有意义的回退环境。通常应该假设一个类似浏览器的环境。

Providing Node.js, WebWorker and browser versions

{
  "type": "module",
  "exports": {
    "node": "./index-node.js",
    "worker": "./index-worker.js",
    "default": "./index.js"
  }
}

Providing Node.js, browser and electron versions

{
  "type": "module",
  "exports": {
    "electron": {
      "node": "./index-electron-node.js",
      "default": "./index-electron.js"
    },
    "node": "./index-node.js",
    "default": "./index.js"
  }
}

Combining patterns

Example 1

这是一个package的例子,它对生产和开发使用进行了优化,并对 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"
  }
}

Example 2

这是一个支持Node.js、浏览器和电子的包的例子,它对生产和开发使用进行了优化,并对 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​ 检测生产/开发。

Guidelines

  • 避免 default 导出。不同工具之间的处理方式不同。只使用命名的导出。

  • 永远不要为不同的条件提供不同的api或语义。
  • 将源代码写成ESM,然后通过babel、typescript或类似的工具编译成CJS。
  • 在package中使用 ​.cjs​ 或 ​type:"commonjs"​。将源代码清晰地标记为CommonJs。这使得如果使用CommonJs或ESM,工具可以静态地检测到它。这对于只支持ESM而不支持CommonJs的工具来说非常重要。
  • 包中使用的ESM支持以下类型的请求:支持模块请求,指向其他包,使用package.json.relative请求,指向包中的其他文件。它们不能指向包外的文件。数据:支持url请求。默认情况下不支持其他绝对请求或服务器相对请求,但某些工具或环境可能支持它们。

Further Reading


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号