Node.js v18.16.0 文档


目录

模块:包#

介绍#

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

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

确定模块系统#

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

  • 扩展名为 .mjs 的文件。

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

  • 字符串作为参数传入 --eval,或通过 STDIN 管道传输到 node,带有标志 --input-type=module

Node.js 会将所有其他形式的输入视为 CommonJS,例如最近的父 package.json 文件不包含顶层 "type" 字段的 .js 文件,或没有标志 --input-type 的字符串输入。 此行为是为了保持向后兼容性。 但是,现在 Node.js 同时支持 CommonJS 和 ES 模块,最好尽可能明确。 当作为初始输入传给 node、或者当被 import 语句或 import() 表达式或 require() 表达式引用时,Node.js 会将以下视为 CommonJS:

  • 扩展名为 .cjs 的文件。

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

  • 字符串作为参数传入 --eval--print,或通过 STDIN 管道传输到 node,带有标志 --input-type=commonjs

包作者应该包括 "type" 字段,即使在所有源都是 CommonJS 的包中也是如此。 如果 Node.js 的默认类型发生变化,显式说明包的 type 将使包面向未来,它还将使构建工具和加载器更容易确定应如何解释包中的文件。

模块加载器#

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 模块,但需要导入断言。
  • 它只接受 JavaScript 文本文件的 .js.mjs.cjs 扩展名。
  • 它可以用来加载 JavaScript CommonJS 模块。 这样的模块通过 cjs-module-lexer 来尝试识别命名的导出,如果可以通过静态分析确定的话是可用的。 导入的 CommonJS 模块将其 URL 转换为绝对路径,然后通过 CommonJS 模块加载器加载。

package.json 和文件扩展名#

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

"module"package.json "type" 值告诉 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" 包中,可以指示 Node.js 通过将特定文件命名为 .cjs 扩展名将其解释为 CommonJS(因为 .js.mjs 文件都被视为 "module" 包中的 ES 模块)。

  • "type": "commonjs" 包中,可以指示 Node.js 将特定文件解释为 ES 模块,方法是将其命名为 .mjs 扩展名(因为 .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 附带了一个名为 核心包 的工具,旨在使所有包管理器在您的环境中透明可用 - 前提是您安装了 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 错误。

这种导出封装为工具的包接口以及处理包的 semver 升级提供了更可靠的保证。 它不是强封装,因为直接要求包的任何绝对子路径(例如 require('/path/to/node_modules/pkg/subpath.js'))仍将加载 subpath.js

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

{
  "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" 字段中的条目必须始终以 # 开头,以确保它们与外部包说明符消除歧义。

例如,可以使用导入字段来获得内部模块条件导出的好处:

// 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 核心中实现 以外的条件字符串。

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

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

  • "types" - 类型系统可以使用它来解析给定导出的类型文件。 应始终首先包括此条件。
  • "deno" - 表示 Deno 平台的变体。
  • "browser" - 任何网络浏览器环境。
  • "react-native" - 将由 React Native 框架(所有平台)匹配。 要针对 Web 的 React Native,应在此条件之前指定 "browser"
  • "development" - 可用于定义仅开发环境入口点,例如在开发模式下运行时提供额外的调试上下文,例如更好的错误消息。 必须始终与 "production" 互斥。
  • "production" - 可用于定义生产环境入口点。 必须始终与 "development" 互斥。

通过向 本节的 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'; 

在 ES 模块和 CommonJS 模块中使用 require 时也可以使用自引用。 例如,这段代码也可以工作:

// ./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" 仅由打包程序使用的场景不同,或者在 Node.js 评估之前将 ES 模块文件动态转换为 CommonJS,ES 模块入口点引用的文件被评估为 ES 模块。

双封装危害#

当应用程序使用提供 CommonJS 和 ES 模块源的包时,如果包的两个版本都被加载,则存在某些错误的风险。 此潜力来自于 const pkgInstance = require('pkg') 创建的 pkgInstanceimport pkgInstance from 'pkg' 创建的 pkgInstance(或像 'pkg/module' 这样的替代主路径)不同的事实。 This is the “dual package hazard,” 同一包的两个版本可以在同一运行时环境中加载。 虽然应用程序或包不太可能有意直接加载两个版本,但应用程序加载一个版本而应用程序的依赖项加载另一个版本是很常见的。 这种危险可能发生,因为 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 模块语法的任何优势。

一个包也可能在 breaking change 版本冲击中从 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' 中的 nameconst { 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 模块语法版本。 确定 CommonJS 版本不会在应用程序的任何地方加载的用户可以通过 import 'pkg/module' 使用它,例如依赖项; 或者如果可以加载 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 和 ES 模块版本之间共享的一个或多个 CommonJS 文件中隔离状态。 比如 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,
    }; 

    即使在应用程序中通过 requireimport 使用 pkg(例如,在应用程序代码中通过 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" - 加载包时的默认模块,如果没有指定exports,在引入exports之前的Node.js版本中。
  • "packageManager" - 包管理器在贡献包时推荐。 由 核心包 垫片利用。
  • "type" - 决定是否将 .js 文件加载为 CommonJS 或 ES 模块的包类型。
  • "exports" - 包导出和条件导出。 当存在时,限制可以从包中加载哪些子模块。
  • "imports" - 包导入,供包本身内的模块使用。

"name"#

{
  "name": "package-name"
} 

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

除了 "exports" 字段之外,"name" 字段还可以用于 自引用 使用其名称的包。

"main"#

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

当通过 node_modules 查找按名称导入时,则 "main" 字段定义了包的入口点。 其值为路径。

当包具有 "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 之外的任何其他东西。

该字段目前处于试验阶段,需要选择加入; 检查 核心包 页面以了解有关该过程的详细信息。

"type"#

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

当最近的父 package.json 文件包含值为 "module" 的顶层字段 "type" 时,以 .js 结尾的文件将作为 ES 模块加载。

最近的父 package.json 定义为在当前文件夹中搜索时找到的第一个 package.json,该文件夹的父文件夹,依此类推,直到到达 node_modules 文件夹或卷根。

// 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"
  }
} 

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

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

该字段为当前包定义 子路径导入