模組:包#

介紹#

一個包(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 欄位中的條目必須是以 # 開頭的字串。

包匯入允許對映到外部包。

此欄位為當前包定義子路徑匯入