模块:包#

简介#

包是一个由 package.json 文件描述的文件夹树。包由包含 package.json 文件的文件夹以及所有子文件夹组成,直到下一个包含另一个 package.json 文件的文件夹,或名为 node_modules 的文件夹。

此页面为编写 package.json 文件的包作者提供指导,并提供对 Node.js 定义的 package.json 字段的参考。

确定模块系统#

简介#

当作为初始输入传递给 node,或由 import 语句或 import() 表达式引用时,Node.js 将将以下内容视为 ES 模块

  • 扩展名为.mjs的文件。

  • 当最近的父级package.json文件包含一个顶层"type"字段,其值为"module"时,扩展名为.js的文件。

  • 使用--eval作为参数传递的字符串,或通过STDIN管道传递到node,并带有--input-type=module标志。

  • 当使用--experimental-detect-module时,包含仅作为ES 模块成功解析的语法的代码,例如importexport语句或import.meta,没有明确的标记来指示其解释方式。明确的标记是.mjs.cjs扩展名、package.json"type"字段,其值为"module""commonjs",或--input-type--experimental-default-type标志。动态import()表达式在 CommonJS 或 ES 模块中都受支持,不会导致文件被视为 ES 模块。

当作为初始输入传递给node,或通过import语句或import()表达式引用时,Node.js 将以下内容视为CommonJS

  • 扩展名为.cjs的文件。

  • 当最近的父级package.json文件包含一个顶层字段"type",其值为"commonjs"时,扩展名为.js的文件。

  • 使用--eval--print作为参数传递的字符串,或通过STDIN管道传递到node,并带有--input-type=commonjs标志。

除了这些明确的情况外,还有其他情况,Node.js 会根据--experimental-default-type标志的值默认使用一个模块系统或另一个模块系统。

  • .js结尾或没有扩展名的文件,如果在同一个文件夹或任何父文件夹中不存在package.json文件。

  • .js结尾或没有扩展名的文件,如果最近的父级package.json字段缺少"type"字段;除非该文件夹位于node_modules文件夹内。(为了向后兼容,node_modules下的包范围在package.json文件缺少"type"字段时始终被视为 CommonJS,无论--experimental-default-type的值如何。)

  • 当未指定 `--input-type` 时,作为 `--eval` 参数传递的字符串或通过 `STDIN` 传递到 `node` 的字符串。

此标志目前默认为 `“commonjs”`,但将来可能会更改为默认 `“module”`。出于这个原因,最好在任何可能的情况下明确说明;特别是,包作者应该始终在他们的 `package.json` 文件中包含 `“type”` 字段,即使在所有源代码都是 CommonJS 的包中也是如此。明确说明包的 `type` 将使包在 Node.js 的默认类型将来发生变化时免受影响,并且还将使构建工具和加载器更容易确定如何解释包中的文件。

模块加载器#

Node.js 有两种系统用于解析说明符和加载模块。

有一个 CommonJS 模块加载器

  • 它是完全同步的。
  • 它负责处理 `require()` 调用。
  • 它是可猴子补丁的。
  • 它支持 文件夹作为模块
  • 在解析说明符时,如果找不到完全匹配项,它将尝试添加扩展名(`.js`、`.json`,最后是 `.node`),然后尝试解析 文件夹作为模块
  • 它将 `.json` 视为 JSON 文本文件。
  • `.node` 文件被解释为使用 `process.dlopen()` 加载的已编译附加模块。
  • 它将所有缺少 `.json` 或 `.node` 扩展名的文件视为 JavaScript 文本文件。
  • 它不能用于加载 ECMAScript 模块(尽管可以从 CommonJS 模块 加载 ECMASCript 模块)。当用于加载不是 ECMAScript 模块的 JavaScript 文本文件时,它会将其作为 CommonJS 模块加载。

有一个 ECMAScript 模块加载器

  • 它是异步的。
  • 它负责处理 `import` 语句和 `import()` 表达式。
  • 它不可猴子补丁,可以使用 加载器钩子 自定义。
  • 它不支持文件夹作为模块,目录索引(例如 `'./startup/index.js'`)必须完全指定。
  • 它不进行扩展名搜索。当说明符是相对或绝对文件 URL 时,必须提供文件扩展名。
  • 它可以加载 JSON 模块,但需要导入断言。
  • 它只接受 `。js`、`。mjs` 和 `。cjs` 扩展名作为 JavaScript 文本文件。
  • 它可以用来加载 JavaScript CommonJS 模块。这些模块会通过 cjs-module-lexer 进行处理,尝试识别命名导出,如果可以通过静态分析确定,则这些导出可用。导入的 CommonJS 模块的 URL 会被转换为绝对路径,然后通过 CommonJS 模块加载器加载。

package.json 和文件扩展名#

在一个包中,package.json "type" 字段定义了 Node.js 如何解释 .js 文件。如果 package.json 文件没有 "type" 字段,.js 文件将被视为 CommonJS

package.json"type" 值为 "module" 表示 Node.js 将解释该包中的 .js 文件为使用 ES 模块 语法。

"type" 字段不仅适用于初始入口点 (node my-app.js),也适用于 import 语句和 import() 表达式引用的文件。

// my-app.js, treated as an ES module because there is a package.json
// file in the same folder with "type": "module".

import './startup/init.js';
// Loaded as ES module since ./startup contains no package.json file,
// and therefore inherits the "type" value from one level up.

import 'commonjs-package';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs". 

.mjs 结尾的文件始终被加载为 ES 模块,无论其最近的父级 package.json 是什么。

.cjs 结尾的文件始终被加载为 CommonJS,无论其最近的父级 package.json 是什么。

import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always loaded as CommonJS.

import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always loaded as ES module. 

.mjs.cjs 扩展名可用于在同一个包中混合类型。

  • 在一个 "type": "module" 包中,可以通过将文件命名为 .cjs 扩展名来指示 Node.js 将特定文件解释为 CommonJS(因为 .js.mjs 文件在 "module" 包中都被视为 ES 模块)。

  • 在一个 "type": "commonjs" 包中,可以通过将文件命名为 .mjs 扩展名来指示 Node.js 将特定文件解释为 ES 模块(因为 .js.cjs 文件在 "commonjs" 包中都被视为 CommonJS)。

--input-type 标志#

当设置 --input-type=module 标志时,作为 --eval(或 -e)参数传递的字符串,或通过 STDIN 传递到 node 的字符串,将被视为 ES 模块

node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

echo "import { sep } from 'node:path'; console.log(sep);" | node --input-type=module 

为了完整性,还存在 --input-type=commonjs,用于明确地将字符串输入作为 CommonJS 运行。如果未指定 --input-type,则这是默认行为。

确定包管理器#

稳定性:1 - 实验性

虽然所有 Node.js 项目在发布后都应该可以被所有包管理器安装,但它们的开发团队通常需要使用一个特定的包管理器。为了简化此过程,Node.js 附带了一个名为 Corepack 的工具,旨在使所有包管理器在您的环境中透明地可用 - 只要您安装了 Node.js。

默认情况下,Corepack 不会强制使用任何特定的包管理器,并将使用与每个 Node.js 版本关联的通用“最后已知良好”版本,但您可以通过在项目的 package.json 中设置 "packageManager" 字段来改善此体验。

包入口点#

在包的 package.json 文件中,两个字段可以定义包的入口点:"main""exports"。这两个字段都适用于 ES 模块和 CommonJS 模块入口点。

所有版本的 Node.js 都支持 "main" 字段,但其功能有限:它只定义包的主入口点。

"exports" 提供了 "main" 的现代替代方案,允许定义多个入口点,支持环境之间的条件入口解析,并阻止除 "exports" 中定义的入口点之外的任何其他入口点。这种封装允许模块作者明确定义其包的公共接口。

对于针对当前支持的 Node.js 版本的新包,建议使用 "exports" 字段。对于支持 Node.js 10 及以下版本的包,需要使用 "main" 字段。如果同时定义了 "exports""main",则在支持的 Node.js 版本中,"exports" 字段优先于 "main"

条件导出 可在 "exports" 中使用,以定义每个环境的不同包入口点,包括包是通过 require 还是通过 import 引用。有关在单个包中支持 CommonJS 和 ES 模块的更多信息,请参阅 双重 CommonJS/ES 模块包部分

现有的引入 "exports" 字段的包将阻止包的使用者使用任何未定义的入口点,包括 package.json(例如 require('your-package/package.json'))。这很可能是一个重大更改。

为了使引入 "exports" 成为非破坏性的,请确保导出所有以前支持的入口点。最好明确指定入口点,以便包的公共 API 定义明确。例如,以前导出 mainlibfeaturepackage.json 的项目可以使用以下 package.exports

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/index": "./lib/index.js",
    "./lib/index.js": "./lib/index.js",
    "./feature": "./feature/index.js",
    "./feature/index": "./feature/index.js",
    "./feature/index.js": "./feature/index.js",
    "./package.json": "./package.json"
  }
} 

或者,项目可以选择使用导出模式导出带有和不带有扩展名的子路径的整个文件夹

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/*": "./lib/*.js",
    "./lib/*.js": "./lib/*.js",
    "./feature": "./feature/index.js",
    "./feature/*": "./feature/*.js",
    "./feature/*.js": "./feature/*.js",
    "./package.json": "./package.json"
  }
} 

通过以上方法为任何次要包版本提供向后兼容性,包的未来重大更改可以将导出限制为仅公开的特定功能导出

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./feature/*.js": "./feature/*.js",
    "./feature/internal/*": null
  }
} 

主入口点导出#

在编写新包时,建议使用 "exports" 字段

{
  "exports": "./index.js"
} 

当定义了 "exports" 字段时,包的所有子路径都被封装,不再对导入者可用。例如,require('pkg/subpath.js') 会抛出 ERR_PACKAGE_PATH_NOT_EXPORTED 错误。

这种对导出的封装为工具和处理包的语义版本升级提供了更可靠的包接口保证。它不是一个强封装,因为对包的任何绝对子路径的直接 require,例如 require('/path/to/node_modules/pkg/subpath.js'),仍然会加载 subpath.js

当前所有支持的 Node.js 版本和现代构建工具都支持 "exports" 字段。对于使用旧版本 Node.js 或相关构建工具的项目,可以通过在 "exports" 旁边包含 "main" 字段来实现兼容性,该字段指向同一个模块

{
  "main": "./index.js",
  "exports": "./index.js"
} 

子路径导出#

使用 "exports" 字段时,可以通过将主入口点视为 "." 子路径来定义自定义子路径以及主入口点

{
  "exports": {
    ".": "./index.js",
    "./submodule.js": "./src/submodule.js"
  }
} 

现在,只有 "exports" 中定义的子路径才能被使用者导入

import submodule from 'es-module-package/submodule.js';
// Loads ./node_modules/es-module-package/src/submodule.js 

而其他子路径将导致错误

import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED 
子路径中的扩展名#

包作者应该在他们的导出中提供带扩展名(import 'pkg/subpath.js')或不带扩展名(import 'pkg/subpath')的子路径。这确保每个导出的模块只有一个子路径,以便所有依赖项都导入相同的规范,从而保持包契约对消费者的清晰度并简化包子路径的补全。

传统上,包倾向于使用不带扩展名的样式,这具有可读性和隐藏包内文件真实路径的优点。

随着导入映射现在为浏览器和其他 JavaScript 运行时提供包解析的标准,使用不带扩展名的样式会导致导入映射定义膨胀。显式文件扩展名可以通过使导入映射利用包文件夹映射来映射多个子路径(如果可能)来避免此问题,而不是为每个包子路径导出提供单独的映射条目。这也反映了在相对和绝对导入规范中使用完整规范路径的要求。

导出语法糖#

如果"."导出是唯一的导出,则"exports"字段为这种情况提供语法糖,作为直接的"exports"字段值。

{
  "exports": {
    ".": "./index.js"
  }
} 

可以写成

{
  "exports": "./index.js"
} 

子路径导入#

除了"exports"字段之外,还有一个包"imports"字段用于创建仅适用于包本身内部导入规范的私有映射。

"imports"字段中的条目必须始终以#开头,以确保它们与外部包规范区分开来。

例如,imports字段可用于获得内部模块条件导出的优势

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} 

其中import '#dep'不会解析外部包dep-node-native(包括其导出的内容),而是解析其他环境中相对于包的本地文件./dep-polyfill.js

"exports"字段不同,"imports"字段允许映射到外部包。

导入字段的解析规则与导出字段类似。

子路径模式#

对于导出或导入数量较少的包,我们建议显式列出每个导出子路径条目。但对于具有大量子路径的包,这可能会导致package.json膨胀和维护问题。

对于这些用例,可以使用子路径导出模式

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js"
  },
  "imports": {
    "#internal/*.js": "./src/internal/*.js"
  }
} 

*映射公开嵌套的子路径,因为它只是一种字符串替换语法。

右侧所有 * 实例将被替换为该值,包括包含任何 / 分隔符的情况。

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z.js';
// Loads ./node_modules/es-module-package/src/internal/z.js 

这是一个直接的静态匹配和替换,没有对文件扩展名进行任何特殊处理。在映射的两侧都包含 "*.js" 将公开的包导出限制为仅 JS 文件。

由于可以通过将右侧目标模式视为针对包内文件列表的 ** glob 来确定包的单个导出,因此导出是静态可枚举的属性在导出模式中得以保留。由于 node_modules 路径在导出目标中是被禁止的,因此这种扩展依赖于包本身的文件。

要从模式中排除私有子文件夹,可以使用 null 目标

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js",
    "./features/private-internal/*": null
  }
} 
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js 

条件导出#

条件导出提供了一种根据特定条件映射到不同路径的方法。它们支持 CommonJS 和 ES 模块导入。

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

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

Node.js 实现以下条件,按从最具体到最不具体的顺序排列,因为条件应按顺序定义

  • "node-addons" - 类似于 "node",匹配任何 Node.js 环境。此条件可用于提供使用原生 C++ 附加模块的入口点,而不是提供更通用的入口点,而该入口点不依赖于原生附加模块。此条件可以通过 --no-addons 标志 禁用。
  • "node" - 匹配任何 Node.js 环境。可以是 CommonJS 或 ES 模块文件。在大多数情况下,明确调用 Node.js 平台是不必要的。
  • "import" - 当包通过 importimport() 加载时,或通过 ECMAScript 模块加载器进行任何顶级导入或解析操作时匹配。适用于目标文件的模块格式无关。始终与 "require" 互斥。
  • "require" - 当包通过 require() 加载时匹配。引用文件应可通过 require() 加载,尽管条件匹配与目标文件的模块格式无关。预期的格式包括 CommonJS、JSON 和原生附加模块,但不包括 ES 模块,因为 require() 不支持它们。始终与 "import" 互斥。
  • "default" - 始终匹配的通用回退。可以是 CommonJS 或 ES 模块文件。此条件应始终放在最后。

"exports" 对象中,键的顺序很重要。在条件匹配过程中,较早的条目具有更高的优先级,并优先于后面的条目。一般规则是,条件应该从最具体到最不具体,按照对象顺序排列

使用 "import""require" 条件可能会导致一些风险,这些风险将在 双重 CommonJS/ES 模块包部分 中进一步解释。

"node-addons" 条件可用于提供使用原生 C++ 附加组件的入口点。但是,可以通过 --no-addons 标志 禁用此条件。使用 "node-addons" 时,建议将 "default" 视为提供更通用入口点的增强功能,例如使用 WebAssembly 而不是原生附加组件。

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

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

定义一个包,其中 require('pkg/feature.js')import 'pkg/feature.js' 可以在 Node.js 和其他 JS 环境之间提供不同的实现。

使用环境分支时,始终尽可能包含 "default" 条件。提供 "default" 条件可确保任何未知 JS 环境能够使用此通用实现,这有助于避免这些 JS 环境不得不假装是现有环境以支持具有条件导出的包。因此,使用 "node""default" 条件分支通常优于使用 "node""browser" 条件分支。

嵌套条件#

除了直接映射之外,Node.js 还支持嵌套条件对象。

例如,要定义一个包,该包仅在 Node.js 中具有双模式入口点,而在浏览器中没有

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

条件继续按照与平面条件相同的顺序进行匹配。如果嵌套条件没有映射,它将继续检查父条件的剩余条件。这样,嵌套条件的行为类似于嵌套的 JavaScript if 语句。

解析用户条件#

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

node --conditions=development index.js 

这将解析包导入和导出中的 "development" 条件,同时根据需要解析现有的 "node""node-addons""default""import""require" 条件。

可以使用重复标记设置任意数量的自定义条件。

社区条件定义#

除了"import""require""node""node-addons""default" 条件之外的条件字符串在 Node.js 核心中的实现默认情况下会被忽略。

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

由于自定义包条件需要明确的定义以确保正确使用,因此以下提供了一份常见已知包条件及其严格定义的列表,以帮助协调生态系统。

  • "types" - 类型系统可以使用它来解析给定导出的类型文件。此条件应始终首先包含。
  • "browser" - 任何 Web 浏览器环境。
  • "development" - 可用于定义仅限开发环境的入口点,例如在开发模式下运行时提供额外的调试上下文,例如更详细的错误消息。必须始终与 "production" 相互排斥。
  • "production" - 可用于定义生产环境的入口点。必须始终与 "development" 相互排斥。

对于其他运行时,平台特定的条件键定义由WinterCG运行时键 提案规范中维护。

可以通过向Node.js 文档的本节提交拉取请求,将新的条件定义添加到此列表中。列出新的条件定义的要求是

  • 定义应对于所有实现者清晰明了。
  • 应清楚地说明为什么需要该条件的用例。
  • 应该存在足够的现有实现使用情况。
  • 条件名称不应与其他条件定义或广泛使用的条件冲突。
  • 条件定义的列出应为生态系统提供协调优势,而这种优势在其他情况下是无法实现的。例如,对于公司特定或应用程序特定的条件,这并不一定适用。
  • 条件应使 Node.js 用户期望它出现在 Node.js 核心文档中。"types" 条件就是一个很好的例子:它并不真正属于 运行时密钥 提议,但非常适合 Node.js 文档。

上述定义可能会在适当的时候移至专门的条件注册表。

使用其名称自引用包#

在包内,可以通过包的名称引用包的 package.json "exports" 字段中定义的值。例如,假设 package.json

// package.json
{
  "name": "a-package",
  "exports": {
    ".": "./index.mjs",
    "./foo.js": "./foo.js"
  }
} 

那么该包中的任何模块都可以引用包本身中的导出

// ./a-module.mjs
import { something } from 'a-package'; // Imports "something" from ./index.mjs. 

只有当 package.json 具有 "exports" 时,自引用才可用,并且只允许导入 "exports"(在 package.json 中)允许的内容。因此,鉴于前面的包,以下代码将生成运行时错误

// ./another-module.mjs

// Imports "another" from ./m.mjs. Fails because
// the "package.json" "exports" field
// does not provide an export named "./m.mjs".
import { another } from 'a-package/m.mjs'; 

在使用 require 时,自引用也可用,无论是在 ES 模块中还是在 CommonJS 模块中。例如,以下代码也将正常工作

// ./a-module.js
const { something } = require('a-package/foo.js'); // Loads from ./foo.js. 

最后,自引用也适用于作用域包。例如,以下代码也将正常工作

// package.json
{
  "name": "@my/package",
  "exports": "./index.js"
} 
// ./index.js
module.exports = 42; 
// ./other.js
console.log(require('@my/package')); 
$ node other.js
42 

双重 CommonJS/ES 模块包#

在 Node.js 引入对 ES 模块的支持之前,包作者通常会在他们的包中包含 CommonJS 和 ES 模块 JavaScript 源代码,其中 package.json "main" 指定 CommonJS 入口点,package.json "module" 指定 ES 模块入口点。这使 Node.js 能够运行 CommonJS 入口点,而捆绑器等构建工具则使用 ES 模块入口点,因为 Node.js 忽略(并且仍然忽略)顶层的 "module" 字段。

Node.js 现在可以运行 ES 模块入口点,并且一个包可以包含 CommonJS 和 ES 模块入口点(通过单独的说明符,例如 'pkg''pkg/es-module',或者通过 条件导出 在同一个说明符中)。与仅由捆绑器使用 "module" 或 ES 模块文件在 Node.js 评估之前被动态转译为 CommonJS 的情况不同,ES 模块入口点引用的文件将作为 ES 模块进行评估。

双包风险#

当应用程序使用提供 CommonJS 和 ES 模块源的包时,如果加载了包的两个版本,则存在某些错误的风险。这种可能性源于 const pkgInstance = require('pkg') 创建的 pkgInstanceimport pkgInstance from 'pkg'(或其他主路径,如 'pkg/module')创建的 pkgInstance 不同。这就是“双包风险”,即同一包的两个版本可以在同一个运行时环境中加载。虽然应用程序或包不太可能故意直接加载两个版本,但应用程序加载一个版本而应用程序的依赖项加载另一个版本的情况很常见。这种风险可能发生是因为 Node.js 支持混合使用 CommonJS 和 ES 模块,并且会导致意外行为。

如果包主导出是一个构造函数,则由两个版本创建的实例的 instanceof 比较返回 false,如果导出是一个对象,则添加到一个对象中的属性(如 pkgInstance.foo = 3)在另一个对象中不存在。这与 importrequire 语句在全 CommonJS 或全 ES 模块环境中分别的工作方式不同,因此对用户来说很令人惊讶。它也与用户使用诸如 Babelesm 之类的工具进行转译时熟悉的行为不同。

编写双包,同时避免或最小化风险#

首先,上一节中描述的风险发生在包同时包含 CommonJS 和 ES 模块源,并且这两个源都提供用于 Node.js 中时,无论是通过单独的主入口点还是导出路径。包也可以编写为,任何版本的 Node.js 仅接收 CommonJS 源,而包可能包含的任何单独的 ES 模块源仅用于其他环境,例如浏览器。这样的包可以使用任何版本的 Node.js,因为 import 可以引用 CommonJS 文件;但它不会提供使用 ES 模块语法的任何优势。

包也可以在 重大变更 版本升级中从 CommonJS 切换到 ES 模块语法。这样做有一个缺点,即最新版本的包只能在支持 ES 模块的 Node.js 版本中使用。

每种模式都有权衡,但有两种广泛的方法可以满足以下条件

  1. 包可以通过 requireimport 使用。
  2. 该包可在当前 Node.js 版本和不支持 ES 模块的旧版 Node.js 版本中使用。
  3. 该包的主入口点,例如 'pkg',既可通过 require 解析为 CommonJS 文件,也可通过 import 解析为 ES 模块文件。(导出路径也是如此,例如 'pkg/feature'。)
  4. 该包提供命名导出,例如 import { name } from 'pkg' 而不是 import pkg from 'pkg'; pkg.name
  5. 该包可能在其他 ES 模块环境(如浏览器)中使用。
  6. 上一节中描述的风险已避免或最小化。
方法 #1:使用 ES 模块包装器#

以 CommonJS 编写包或将 ES 模块源代码转译为 CommonJS,并创建一个 ES 模块包装器文件,用于定义命名导出。使用 条件导出,ES 模块包装器用于 import,而 CommonJS 入口点用于 require

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./wrapper.mjs",
    "require": "./index.cjs"
  }
} 

前面的示例使用显式扩展名 .mjs.cjs。如果您的文件使用 .js 扩展名,"type": "module" 将导致这些文件被视为 ES 模块,就像 "type": "commonjs" 会导致它们被视为 CommonJS 一样。请参阅 启用

// ./node_modules/pkg/index.cjs
exports.name = 'value'; 
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name; 

在此示例中,来自 import { name } from 'pkg'name 与来自 const { name } = require('pkg')name 是同一个单例。因此,当比较两个 name 时,=== 返回 true,从而避免了不同的说明符风险。

如果模块不仅仅是一个命名导出的列表,而是包含一个唯一的函数或对象导出,例如 module.exports = function () { ... },或者如果需要在包装器中支持 import pkg from 'pkg' 模式,那么包装器将改为编写为可选地导出默认值以及任何命名导出

import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule; 

此方法适用于以下任何用例

  • 该包目前以 CommonJS 编写,作者希望避免将其重构为 ES 模块语法,但希望为 ES 模块使用者提供命名导出。
  • 该包具有其他依赖于它的包,最终用户可能会同时安装该包和其他包。例如,utilities 包直接在应用程序中使用,而 utilities-plus 包为 utilities 添加了更多功能。由于包装器导出底层的 CommonJS 文件,因此 utilities-plus 是以 CommonJS 还是 ES 模块语法编写并不重要;无论哪种方式都可以正常工作。
  • 该包存储内部状态,并且包作者希望避免重构包以隔离其状态管理。请参阅下一节。

此方法的一种变体不需要为消费者提供条件导出,可以添加一个导出,例如 "./module",指向包的全部 ES 模块语法版本。这可以通过 import 'pkg/module' 被那些确信 CommonJS 版本不会在应用程序中的任何地方加载的用户使用,例如通过依赖项;或者如果 CommonJS 版本可以加载但不会影响 ES 模块版本(例如,因为包是无状态的)。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./wrapper.mjs"
  }
} 
方法 #2:隔离状态#

一个 package.json 文件可以直接定义单独的 CommonJS 和 ES 模块入口点

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
} 

如果包的 CommonJS 和 ES 模块版本等效,则可以执行此操作,例如因为一个是另一个的转译输出;并且包的状态管理被仔细隔离(或者包是无状态的)。

状态成为问题的原因是,包的 CommonJS 和 ES 模块版本都可能在应用程序中使用;例如,用户的应用程序代码可以 import ES 模块版本,而依赖项 require CommonJS 版本。如果发生这种情况,包的两个副本将加载到内存中,因此将存在两个独立的状态。这很可能会导致难以排查的错误。

除了编写无状态包(例如,如果 JavaScript 的 Math 是一个包,它将是无状态的,因为它的所有方法都是静态的),还有一些方法可以隔离状态,以便它在包的潜在加载的 CommonJS 和 ES 模块实例之间共享

  1. 如果可能,将所有状态包含在实例化的对象中。例如,JavaScript 的 Date 需要实例化才能包含状态;如果它是一个包,它将像这样使用

    import Date from 'date';
    const someDate = new Date();
    // someDate contains state; Date does not 

    new 关键字不是必需的;包的函数可以返回一个新对象,或者修改传入的对象,以将状态保持在包外部。

  2. 将状态隔离在一个或多个 CommonJS 文件中,这些文件在包的 CommonJS 和 ES 模块版本之间共享。例如,如果 CommonJS 和 ES 模块入口点分别是 index.cjsindex.mjs

    // ./node_modules/pkg/index.cjs
    const state = require('./state.cjs');
    module.exports.state = state; 
    // ./node_modules/pkg/index.mjs
    import state from './state.cjs';
    export {
      state,
    }; 

    即使 pkg 在应用程序中通过 requireimport 都使用(例如,通过应用程序代码中的 import 和通过依赖项中的 require),pkg 的每个引用都将包含相同的状态;并且从任一模块系统修改该状态将应用于两者。

任何附加到包的单例的插件都需要分别附加到 CommonJS 和 ES 模块单例。

此方法适用于以下任何用例

  • 该包目前使用 ES 模块语法编写,包作者希望在支持这种语法的任何地方使用该版本。
  • 该包是无状态的,或者它的状态可以很容易地隔离。
  • 该包不太可能拥有其他依赖于它的公共包,或者如果有,该包是无状态的,或者其状态不需要在依赖项之间或与整个应用程序共享。

即使状态隔离,在包的 CommonJS 和 ES 模块版本之间仍然存在可能的额外代码执行成本。

与之前的方法一样,这种方法的变体不需要为消费者提供条件导出,可以添加一个导出,例如 "./module",指向包的纯 ES 模块语法版本。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./index.mjs"
  }
} 

Node.js package.json 字段定义#

本节介绍 Node.js 运行时使用的字段。其他工具(例如 npm)使用 Node.js 忽略的额外字段,此处未记录。

package.json 文件中的以下字段在 Node.js 中使用

  • "name" - 在包内使用命名导入时相关。还被包管理器用作包的名称。
  • "main" - 加载包时的默认模块,如果未指定导出,以及在 Node.js 版本早于引入导出之前。
  • "packageManager" - 向包贡献时推荐的包管理器。由 Corepack 垫片利用。
  • "type" - 包类型,确定是否将 .js 文件加载为 CommonJS 或 ES 模块。
  • "exports" - 包导出和条件导出。如果存在,则限制可以从包内加载哪些子模块。
  • "imports" - 包导入,供包本身内的模块使用。

"name"#

{
  "name": "package-name"
} 

"name" 字段定义了你的包的名称。发布到 npm 注册表需要一个满足 特定要求 的名称。

"name" 字段可以与 "exports" 字段一起使用,以 自引用 使用其名称的包。

"main"#

{
  "main": "./index.js"
} 

"main" 字段定义了通过 node_modules 查找按名称导入的包的入口点。它的值是一个路径。

当一个包具有 "exports" 字段时,在按名称导入包时,它将优先于 "main" 字段。

它还定义了当 包目录通过 require() 加载 时使用的脚本。

// This resolves to ./path/to/directory/index.js.
require('./path/to/directory'); 

"packageManager"#

稳定性:1 - 实验性

{
  "packageManager": "<package manager name>@<version>"
} 

"packageManager" 字段定义了在当前项目中工作时预期使用的包管理器。它可以设置为任何 受支持的包管理器,并将确保您的团队使用完全相同的包管理器版本,而无需安装除 Node.js 之外的任何其他内容。

此字段目前处于实验阶段,需要选择加入;有关该过程的详细信息,请查看 Corepack 页面。

"type"#

"type" 字段定义了 Node.js 用于所有具有该 package.json 文件作为其最近父级的 .js 文件的模块格式。

当最近的父级 package.json 文件包含一个名为 "type" 的顶级字段,且其值为 "module" 时,以 .js 结尾的文件将被加载为 ES 模块。

最近的父级 package.json 被定义为在当前文件夹、该文件夹的父级以及向上搜索直到遇到 node_modules 文件夹或卷根目录时找到的第一个 package.json 文件。

// package.json
{
  "type": "module"
} 
# In same folder as preceding package.json
node my-app.js # Runs as ES module 

如果最近的父级 package.json 缺少 "type" 字段,或包含 "type": "commonjs",则 .js 文件将被视为 CommonJS。如果到达卷根目录且未找到 package.json,则 .js 文件将被视为 CommonJS

如果最近的父级 package.json 包含 "type": "module",则 .js 文件的 import 语句将被视为 ES 模块。

// my-app.js, part of the same example as above
import './startup.js'; // Loaded as ES module because of package.json 

无论 "type" 字段的值如何,.mjs 文件始终被视为 ES 模块,而 .cjs 文件始终被视为 CommonJS。

"exports"#

{
  "exports": "./index.js"
} 

"exports" 字段允许定义包的 入口点,当通过 node_modules 查找或 自引用 其自身名称加载时。它在 Node.js 12+ 中作为 "main" 的替代方案得到支持,可以支持定义 子路径导出条件导出,同时封装内部未导出的模块。

条件导出 也可以在 "exports" 中使用,以定义每个环境的不同包入口点,包括包是通过 require 还是通过 import 引用。

"exports" 中定义的所有路径必须是相对于文件的 URL,以 ./ 开头。

"imports"#

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} 

imports 字段中的条目必须是以 # 开头的字符串。

包导入允许映射到外部包。

此字段定义当前包的子路径导入