模組:套件#

簡介#

套件是一個由 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 作為初始輸入傳遞給 node,或由 import 陳述式或 import() 表達式參照時,Node.js 將把下列內容視為 CommonJS

  • 具有 .cjs 副檔名的檔案。

  • 當最近的父 package.json 檔案包含頂層欄位 "type",且其值為 "commonjs" 時,具有 .js 副檔名的檔案。

  • 以字串作為引數傳遞給 --eval--print,或透過 STDIN 傳遞給 node,並加上旗標 --input-type=commonjs

除了這些明確的情況之外,還有其他情況,Node.js 會根據 --experimental-default-type 旗標的值,預設為一個或另一個模組系統。

  • 如果同一個資料夾或任何父資料夾中沒有 package.json 檔案,則以 .js 結尾或沒有副檔名的檔案。

  • 如果最近的父 package.json 欄位缺少 "type" 欄位,則以 .js 結尾或沒有副檔名的檔案;除非資料夾在 node_modules 資料夾內。(為了向後相容,當 package.json 檔案缺少 "type" 欄位時,node_modules 下的套件範圍始終視為 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 模組,但需要匯入斷言。
  • 它僅接受 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 标志#

傳遞給 --eval (或 -e) 作為參數的字串,或透過 STDIN 傳遞給 node 的字串,在設定 --input-type=module 旗標時會被視為 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 模組進入點。

"main" 欄位在所有版本的 Node.js 中都受到支援,但其功能有限:它只定義套件的主要進入點。

"exports" 提供了一個現代化的替代方案,用於 "main",允許定義多個進入點、環境之間條件式進入點解析支援,以及防止在 "exports" 中定義的進入點以外的任何其他進入點。此封裝允許模組作者明確定義其套件的公用介面。

對於針對目前支援的 Node.js 版本的新套件,建議使用 "exports" 欄位。對於支援 Node.js 10 及以下版本的套件,則需要 "main" 欄位。如果 "exports""main" 都已定義,則 "exports" 欄位會優先於 "main" 在支援的 Node.js 版本中。

條件式輸出可以在 "exports" 中使用,以定義每個環境的不同套件進入點,包括套件是透過 requireimport 參照。如需關於在單一套件中同時支援 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') 的子路徑。這可確保每個匯出的模組只有一個子路徑,讓所有相依項匯入相同的明確指定符,保持套件合約對消費者而言清楚,並簡化套件子路徑完成。

傳統上,套件傾向使用不帶擴充的樣式,這具有可讀性和遮蔽套件中檔案真實路徑的優點。

現在 匯入地圖 為瀏覽器和其他 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" 條件。

可以使用重複旗標設定任何數量的自訂條件。

社群條件定義#

除了 在 Node.js 核心實作"import""require""node""node-addons""default" 條件之外的其他條件字串,預設會被忽略。

其他平台可能會實作其他條件,而且使用者條件可以在 Node.js 中透過 --conditions / -C 旗標 啟用。

由於自訂套件條件需要明確的定義才能確保正確使用,因此以下提供常見已知套件條件及其嚴格定義的清單,以協助生態系統協調。

  • "types" - 可供輸入系統用來解析給定匯出的輸入檔案。此條件應始終優先包含。
  • "browser" - 任何網頁瀏覽器環境。
  • "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'; 

在 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" 僅由打包器使用,或 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 和舊版的 Node.js(不支援 ES 模組)中使用。
  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",指向該套件的 all-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,
    }; 

    即使在應用程式中同時透過 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" - 如果未指定匯出,且在 Node.js 導入匯出之前版本中,則載入套件時的預設模組。
  • "packageManager" - 為套件做出貢獻時建議的套件管理員。由 Corepack shim 採用。
  • "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" 時,以 ES 模組載入以 .js 結尾的檔案。

最近的父項 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" 中使用,以定義每個環境的不同套件進入點,包括套件是透過 requireimport 參照的。

"exports" 中定義的所有路徑都必須是從 ./ 開始的相對檔案 URL。

"imports"#

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

imports 欄位中的項目必須是以 # 開頭的字串。

套件匯入允許對應到外部套件。

此欄位定義目前套件的 子路徑匯入