让我看看有多少人不知道 package.json 中的条件导出

391 阅读7分钟

条件导出提供了一种根据特定条件映射到不同路径的方式。比如可以根据 CommonJS 还是 ES 模块提供不同的导入文件。

例如,对于一个想要为 require()import 提供不同 ES 模块导出的包,可以这样编写:

// package.json
{
  "exports": {
    "import": "./index-module.js",
    "require": "./index-require.cjs"
  },
  "type": "module"
}

Node.js 实现了以下条件,按照从最具体到最不具体的顺序列出,exports 字段中定义的条件导出也应按照这样的顺序定义:

  • node-addons - 类似于 node,适用于任何 Node.js 环境。这个条件可以用来提供一个使用原生 C++ 插件的入口点,而不是更通用且不依赖原生插件的入口点。可以通过 --no-addons 标志禁用此条件。

Node.js 原生插件是指那些用 C++ 编写的库,它们可以扩展 Node.js 的功能,提供对底层系统资源的访问,或者执行需要更高性能的操作。

  • node - 匹配任何 Node.js 环境。可以是 CommonJS 或 ES 模块文件。在大多数情况下,不需要明确指定 Node.js 平台

  • import - 当包通过 importimport() 加载,或通过 ECMAScript 模块加载器的任何顶层导入或解析操作加载时匹配。无论目标文件的模块格式如何,该条件始终与 require 互斥。

  • module-sync - 无论包是通过 importimport() 还是 require() 加载时都匹配。期望的格式是不包含顶层 await 的 ES 模块;如果包含,则在调用 require() 时会抛出 ERR_REQUIRE_ASYNC_MODULE 错误。

  • default - 是一个通用匹配条件,当以上条件都不匹配时,则会回退到此条件。可以是 CommonJS 或 ES 模块文件。此条件应始终放在最后。

exports 对象中,键(key)的顺序非常重要。在进行条件匹配时,前面的条目具有更高的优先级,并会优先处理。在一般情况下,条件应按照从最具体到最不具体的顺序排列。

node-addons 条件可以用于提供一个使用原生 C++ 插件的入口点。不过,这个条件可以通过 --no-addons 标志禁用。当使用 node-addons ,建议增加 default 条件,然后在 default 中增加更通用的入口点,例如使用 WebAssembly 而不是原生插件。

条件导出也可以扩展到导出子路径,例如:

{
  "exports": {
    ".": "./index.js",
    "./feature.js": {
      "node": "./feature-node.js",
      "default": "./feature.js"
    }
  }
}

上面代码定义了一个包,其中 require('pkg/feature.js')import 'pkg/feature.js' 可以在 Node.js 和其他 JavaScript 环境中提供不同的实现。

为了确保包在各种 JavaScript 环境中都能正常工作,建议在条件导出配置中包含 default 条件,提供 npm 包的兼容性和灵活性。

嵌套条件

package.json 文件的 exports 字段中,不仅可以使用直接映射(即简单的键值对,指定特定条件下应加载的文件路径),还可以使用嵌套的条件对象来定义更复杂的导出规则。这种方式允许你根据多个条件组合来指定模块的具体实现路径。

例如,要定义一个仅在 Node.js 中使用(而非在浏览器中使用)的具有双模式入口点(即 CommonJS 和 ES 模块)的包:

{
  "exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs"
  }
}

在处理 package.json 文件中的 exports 字段时,无论是扁平条件还是嵌套条件,都会按照定义的顺序进行匹配。如果某个嵌套条件没有找到匹配的映射(即没有对应的文件路径),解析器会继续检查同一层级下的其他条件或回退到父条件的下一个分支。这种机制使得嵌套条件的行为类似于嵌套的 JavaScript if 语句。

解析用户条件

在运行 Node.js 时,可以使用 --conditions 标志添加自定义用户条件:

node --conditions=development index.js

这样在处理包的导入和导出时,就会解析 development 条件,同时根据需要解析现有的 nodenode-addonsdefaultimportrequire 条件。

可以使用重复的条件标志设置任意数量的自定义条件,如果已经找到匹配项,不再检查后续的条件。

node --conditions=dev1 --conditions=prod app.js

通常,exports 定义的条件应由字母数字字符组成,并在必要时使用 :-= 作为分隔符。使用其他字符可能会在 Node.js 之外的环境中遇到兼容性问题。

在 Node.js 中,exports 定义条件的限制极少,但具体来说,这些限制包括:

  1. 它们必须至少包含一个字符
  2. 它们不能以 . 开头,因为它们可能会出现在也允许相对路径的地方。
  3. 它们不能包含 ,,因为某些命令行工具可能会将其解析为逗号分隔的列表。
  4. 它们不能是像 10 这样的整数属性键,因为这可能会对 JavaScript 对象的属性键排序产生意外影响。

案例1

这里给个具体的解析用户条件的案例,此案例的目录结构为:

90.png

node_modules 文件夹是自己创建的,用于模拟真实的项目开发环境。

node_modules/common/package.json 文件内容为:

{
  "name": "common",
  "exports": {
    ".": {
      "dev": "./dev-entry.js",
      "prod": "./prod-entry.js"
    }
  }
}

node_modules/common/dev-entry.js 文件内容为:

console.log('开发环境入口')

function add(a, b) {
  return a + b;
}

module.exports = {
  add: add,
};

node_modules/common/prod-entry.js 文件内容为:

console.log('生产环境入口')

function add(a, b) {
  return a + b;
}

module.exports = {
  add: add,
};

app.js 文件下引入 common 模块包:

const common = require("common")

const total = common.add(2, 3)

console.log('total ', total)

然后在终端运行 app.js 文件:

91.png

使用重复的 --conditions 标志来指定多个自定义条件,如果已经找到匹配项,不再检查后续的条件

92.png

案例2

案例 2 演示 exports 定义的条件可以使用 :-= 作为分隔符。

案例2 的目录结构为:

93.png

node_modules/common/package.json 文件内容为:

{
  "name": "common",
  "exports": {
    ".": {
      "dev-prod": "./dev-prod.js",
      "os:win32": "./win32.js",
      "require=cjs": "./cjs.js"
    }
  }
}

node_modules/common/cjs.js 文件内容为:

console.log('cjs入口')

function add(a, b) {
  return a + b;
}

module.exports = {
  add: add,
};

node_modules/common/dev-prod.js 文件内容为:

console.log('dev-prod入口')

function add(a, b) {
  return a + b;
}

module.exports = {
  add: add,
};

node_modules/common/win32.js 文件内容为:

console.log('win32入口')

function add(a, b) {
  return a + b;
}

module.exports = {
  add: add,
};

app.js 文件内容为:

const common = require("common")

const total = common.add(2, 3)

console.log('total ', total)

然后在命令行终端指定条件为 require=cjs 执行 app.js 文件:

94.png

社区条件定义

除了 Node.js 实现的 importrequirenodemodule-syncnode-addonsdefault 条件之外,其他条件字符串默认会被忽略。

其他平台可能实现其他条件,用户自定他条件可以通过 Node.js 的 --conditions-C 标志启用。

由于自定义包条件需要清晰的定义以确保正确使用,以下提供了一份社区提供的常见已知包条件及其严格定义的列表:

  • types - 用于指定类型系统(如 TypeScript)在解析模块时应该使用的类型声明文件。为了确保类型信息能够正确加载,这个条件应当总是首先列出。通过这种方式,开发者可以在开发过程中获得更好的类型检查和代码补全支持,从而提高开发效率和代码质量。

  • browser - 任何 Web 浏览器环境。通过这种方式,开发者可以为浏览器环境提供特定的模块实现,以确保最佳性能和兼容性。

  • development - 用于定义仅在开发环境中使用的入口点,例如提供额外的调试上下文,如在开发模式下运行时获得更完整的错误消息。始终与 production 互斥。

  • production - 用于定义生产环境的入口点,通常经过优化以提高性能和减少资源占用,并且移除了调试信息。始终与 development 互斥。

对于其他 JS 运行时,特定于平台的条件键(key)定义由 WinterCGRuntime Keys 提案规范中维护。

总结

package.json 中的 exports 字段进行 npm 包模块的条件导出,比如根据 CommonJS 还是 ES 模块提供不同的导入文件:

// package.json
{
  "exports": {
    "import": "./index-module.js",
    "require": "./index-require.cjs"
  },
  "type": "module"
}

exports 字段中的条件导出,应按照最具体到最不具体的顺序列出。

Node.js 自身实现了 node-addonsnodeimportmodule-syncdefault 共 5 个条件属性。

当然,Node.js 也支持用户自定义条件,在执行代码前可以使用 --conditions 标志添加用户自定义的条件。

社区常见的 Node.js 条件属性有 typesbrowserdevelopmentproduction

exports 字段支持嵌套对象,即支持嵌套条件导出,类似于嵌套的 JavaScript if 语句。