模块:包#

介绍#

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

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

确定模块系统#

介绍#

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

  • 扩展名为 .mjs 的文件。

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

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

  • 包含只能被成功解析为 ES 模块 的语法的代码,例如 importexport 语句或 import.meta,且没有明确标记其应如何解释。明确的标记包括 .mjs.cjs 扩展名、package.json 中值为 "module""commonjs""type" 字段,或 --input-type 标志。动态 import() 表达式在 CommonJS 或 ES 模块中都受支持,不会强制将文件视为 ES 模块。请参阅语法检测

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

  • 扩展名为 .cjs 的文件。

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

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

  • 扩展名为 .js 的文件,没有父级 package.json 文件,或者最近的父级 package.json 文件缺少 type 字段,并且代码可以作为 CommonJS 成功执行。换句话说,Node.js 会首先尝试将这类“不明确的”文件作为 CommonJS 运行,如果作为 CommonJS 执行失败(因为解析器发现了 ES 模块语法),则会重试将其作为 ES 模块执行。

在“不明确的”文件中编写 ES 模块语法会带来性能开销,因此鼓励作者尽可能明确。特别是,包作者应始终在其 package.json 文件中包含 "type" 字段,即使在所有源文件都是 CommonJS 的包中也是如此。明确包的 type 将使包在 Node.js 的默认类型将来发生变化时也能保持兼容,并且还会使构建工具和加载器更容易确定包中文件应如何解释。

语法检测#

稳定性:1.2 - 候选发布

Node.js 会检查不明确输入的源代码,以确定其是否包含 ES 模块语法;如果检测到此类语法,该输入将被视为 ES 模块。

不明确的输入定义为:

  • 扩展名为 .js 或无扩展名的文件;并且没有起控制作用的 package.json 文件,或者该文件缺少 type 字段。
  • 字符串输入(--evalSTDIN),且未指定 --input-type

ES 模块语法定义为在作为 CommonJS 执行时会抛出错误的语法。这包括以下内容:

  • import 语句(但不包括 import() 表达式,它在 CommonJS 中是有效的)。
  • export 语句。
  • import.meta 引用。
  • 模块顶层的 await
  • 对 CommonJS 包装变量(requiremoduleexports__dirname__filename)的词法重复声明。

模块加载器#

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

一个是 CommonJS 模块加载器:

  • 它是完全同步的。
  • 它负责处理 require() 调用。
  • 它是可以被 monkey patch 的。
  • 它支持将文件夹作为模块
  • 在解析说明符时,如果找不到完全匹配项,它会尝试添加扩展名(.js.json,最后是 .node),然后尝试将文件夹作为模块进行解析。
  • 它将 .json 文件视为 JSON 文本文件。
  • .node 文件被解释为使用 process.dlopen() 加载的已编译的插件模块。
  • 它将所有缺少 .json.node 扩展名的文件都视为 JavaScript 文本文件。
  • 只有当模块图是同步的(即不包含顶层 await)时,它才能用于从 CommonJS 模块加载 ECMAScript 模块。当用于加载非 ECMAScript 模块的 JavaScript 文本文件时,该文件将作为 CommonJS 模块加载。

另一个是 ECMAScript 模块加载器:

  • 它是异步的,除非它被用于为 require() 加载模块。
  • 它负责处理 import 语句和 import() 表达式。
  • 它不可以被 monkey patch,但可以使用加载器钩子进行自定义。
  • 它不支持将文件夹作为模块,必须完整指定目录索引(例如 './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

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(因为在 "module" 包中,.js.mjs 文件都被视为 ES 模块)。

  • "type": "commonjs" 的包内,可以通过将特定文件命名为 .mjs 扩展名来指示 Node.js 将其解释为 ES 模块(因为在 "commonjs" 包中,.js.cjs 文件都被视为 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,这是默认行为。

包入口点#

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

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

"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 包的任何绝对子路径,如 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')的子路径。这确保了每个导出的模块只有一个子路径,从而所有依赖项都导入相同的一致说明符,使包的约定对消费者清晰,并简化了包子路径的自动补全。

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

随着 import maps 现在为浏览器和其他 JavaScript 运行时中的包解析提供了标准,使用无扩展名的风格可能导致 import map 定义臃肿。明确的文件扩展名可以避免这个问题,它允许 import map 利用包文件夹映射来尽可能多地映射子路径,而不是为每个包子路径导出都创建一个单独的映射条目。这也与在相对和绝对导入说明符中使用完整说明符路径的要求相呼应。

导出目标的路径规则和验证#

"exports" 字段中将路径定义为目标时,Node.js 强制执行多项规则以确保安全性、可预测性和适当的封装。理解这些规则对于发布包的作者至关重要。

目标必须是相对 URL#

"exports" 映射中的所有目标路径(与导出键关联的值)必须是以 ./ 开头的相对 URL 字符串。

// package.json
{
  "name": "my-package",
  "exports": {
    ".": "./dist/main.js",          // Correct
    "./feature": "./lib/feature.js", // Correct
    // "./origin-relative": "/dist/main.js", // Incorrect: Must start with ./
    // "./absolute": "file:///dev/null", // Incorrect: Must start with ./
    // "./outside": "../common/util.js" // Incorrect: Must start with ./
  }
} 

这种行为的原因包括:

  • 安全性: 防止从包自身目录之外导出任意文件。
  • 封装性: 确保所有导出的路径都相对于包根目录解析,使包自成一体。
禁止路径遍历或无效段#

导出目标不得解析到包根目录之外的位置。此外,路径段如 .(单点)、..(双点)或 node_modules(及其 URL 编码等效形式)通常不允许出现在 target 字符串中初始 ./ 之后以及替换到目标模式中的任何 subpath 部分中。

// package.json
{
  "name": "my-package",
  "exports": {
    // ".": "./dist/../../elsewhere/file.js", // Invalid: path traversal
    // ".": "././dist/main.js",             // Invalid: contains "." segment
    // ".": "./dist/../dist/main.js",       // Invalid: contains ".." segment
    // "./utils/./helper.js": "./utils/helper.js" // Key has invalid segment
  }
} 

导出语法糖#

如果 "." 导出是唯一的导出,"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" 字段允许映射到外部包。

"imports" 字段的解析规则在其他方面与 "exports" 字段类似。

子路径模式#

对于只有少量导出或导入的包,我们建议明确列出每个导出子路径条目。但对于有大量子路径的包,这可能会导致 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 模块。始终与 "import" 互斥。
  • "module-sync" - 无论包是通过 importimport() 还是 require() 加载都匹配。格式应为 ES 模块,且其模块图中不包含顶层 await——如果包含,当模块被 require() 时将抛出 ERR_REQUIRE_ASYNC_MODULE
  • "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" 条件。

可以通过重复标志设置任意数量的自定义条件。

典型的条件应只包含字母数字字符,必要时可使用 ":"、"-" 或 "=" 作为分隔符。其他任何字符都可能在 Node.js 之外遇到兼容性问题。

在 Node.js 中,条件几乎没有限制,但具体包括:

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

社区条件定义#

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

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

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

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

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

可以通过向此部分的 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. 

最后,自引用也适用于带作用域的包(scoped packages)。例如,这段代码也能工作:

// 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 package.json 字段定义#

本节描述了 Node.js 运行时使用的字段。其他工具(如 npm)使用额外的字段,这些字段被 Node.js 忽略,且此处不作记录。

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

  • "name" - 在包内使用命名导入时相关。也被包管理器用作包的名称。
  • "main" - 当加载包时,如果没有指定 exports,以及在引入 exports 之前的 Node.js 版本中,这是默认模块。
  • "type" - 包的类型,决定是将 .js 文件作为 CommonJS 还是 ES 模块加载。
  • "exports" - 包导出和条件导出。当存在时,限制可以从包内部加载哪些子模块。
  • "imports" - 包导入,供包内部的模块使用。

"name"#

{
  "name": "package-name"
} 

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

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

"main"#

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

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

当一个包有 "exports" 字段时,按名称导入该包时,"exports" 字段的优先级将高于 "main" 字段。

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

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

"type"#

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

当最近的父 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"
  }
} 

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

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

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