套件的 package.json
中的 exports
欄位允許宣告在使用模組要求時,例如 import "package"
或 import "package/sub/path"
,應該使用哪個模組。它取代了預設實作,分別傳回 main
欄位或 index.js
檔案,用於 "package"
和 "package/sub/path"
的檔案系統查詢。
當指定 exports
欄位時,只有這些模組要求可用。任何其他要求都會導致 ModuleNotFound 錯誤。
一般來說,exports
欄位應包含一個物件,其中每個屬性都指定模組要求的子路徑。對於上述範例,可以使用下列屬性:"."
表示 import "package"
,"./sub/path"
表示 import "package/sub/path"
。以 /
結尾的屬性會將具有此字首的要求轉送至舊檔案系統查詢演算法。對於以 *
結尾的屬性,*
可能採用任何值,屬性值中的任何 *
都會以採用的值取代。
範例
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
模組要求 | 結果 |
---|---|
package | .../package/main.js |
package/sub/path | .../package/secondary.js |
package/prefix/some/file.js | .../package/directory/some/file.js |
package/prefix/deep/file.js | .../package/other-directory/file.js |
package/other-prefix/deep/file.js | .../package/yet-another/deep/file/deep/file.js |
package/main.js | 錯誤 |
套件作者可以提供結果清單,而不是提供單一結果。在這種情況下,此清單會依序嘗試,並使用第一個有效的結果。
注意:只會使用第一個有效的結果,而不是所有有效的結果。
範例
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
在此,package/things/apple
可能會在 .../package/good-things/apple
或 .../package/bad-things/apple
中找到。
套件作者可以讓模組系統根據環境條件選擇一個,而不是直接在 exports
欄位中提供結果。
在這種情況下,應該使用將條件對應到結果的物件。條件會按照物件順序嘗試。包含無效結果的條件會被略過。條件可能會巢狀以建立邏輯 AND。物件中的最後一個條件可能是特殊的 "default"
條件,它總是會相符。
範例
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
這會轉換成類似以下的內容
if (red && valid('./stop.js')) return './stop.js';
if (yellow && valid('./stop.js')) return './stop.js';
if (green) {
if (free && valid('./drive.js')) return './drive.js';
if (valid('./wait.js')) return './wait.js';
}
if (valid('./drive-carefully.js')) return './drive-carefully.js';
throw new ModuleNotFoundError();
可用的條件會根據使用的模組系統和工具而有所不同。
當只支援套件中的一個單一項目 ("."
) 時,可以省略 { ".": ... }
物件巢狀。
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
在每個鍵都是條件的物件中,屬性的順序很重要。條件會按照指定的順序處理。
範例:{ "red": "./stop.js", "green": "./drive.js" }
!= { "green": "./drive.js", "red": "./stop.js" }
(當 red
和 green
條件都設定時,會使用第一個屬性)
在每個鍵都是子路徑的物件中,屬性(子路徑)的順序並不重要。會優先使用較具體的路徑,而非較不具體的路徑。
範例:{ "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" }
== { "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" }
(順序永遠會是:./a/b/c
> ./a/b/
> ./a/
)
exports
欄位優先於其他套件項目欄位,例如 main
、module
、browser
或自訂欄位。
功能 | 支援 |
---|---|
"." 屬性 | Node.js、webpack、rollup、esinstall、wmr |
一般屬性 | Node.js、webpack、rollup、esinstall、wmr |
以 / 結尾的屬性 | Node.js(1)、webpack、rollup、esinstall(2)、wmr(3) |
以 * 結尾的屬性 | Node.js、webpack、rollup、esinstall |
替代方案 | Node.js、webpack、rollup、 |
僅路徑縮寫 | Node.js、webpack、rollup、esinstall、wmr |
僅條件縮寫 | Node.js、webpack、rollup、esinstall、wmr |
條件式語法 | Node.js、webpack、rollup、esinstall、wmr |
巢狀條件語法 | Node.js、webpack、rollup、wmr(5) |
條件順序 | Node.js、webpack、rollup、wmr(6) |
"default" 條件 | Node.js、webpack、rollup、esinstall、wmr |
路徑順序 | Node.js、webpack、rollup |
未對應時的錯誤 | Node.js、webpack、rollup、esinstall、wmr(7) |
混合條件和路徑時的錯誤 | Node.js、webpack、rollup |
(1) 已在 Node.js 中棄用,應優先使用 *
。
(2) "./"
故意忽略為鍵。
(3) 忽略屬性值,並使用屬性鍵作為目標。實際上只允許鍵和值相同的對應。
(4) 支援此語法,但始終使用第一個項目,這使得它無法用於任何實際用例。
(5) 錯誤處理備用同層父條件。
(6) 對於 require
條件,物件順序處理不正確。這是故意的,因為 wmr 沒有區分參照語法。
(7) 使用 "exports": "./file.js"
縮寫時,任何請求例如 package/not-existing
都會解析為該縮寫。如果不使用縮寫,直接檔案存取例如 package/file.js
就不會導致錯誤。
根據用於參照模組的語法設定這些條件之一
條件 | 說明 | 支援 |
---|---|---|
import | 請求來自 ESM 語法或類似語法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
require | 請求來自 CommonJs/AMD 語法或類似語法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
樣式 | 要求來自樣式表參考。 | |
sass | 要求來自 sass 樣式表參考。 | |
資源 | 要求來自資源參考。 | |
指令碼 | 要求來自沒有模組系統的正常指令碼標籤。 |
這些條件也可能另外設定
條件 | 說明 | 支援 |
---|---|---|
模組 | 允許參考 JavaScript 的所有模組語法都支援 ESM。 (僅與 import 或 require 結合使用) | webpack、rollup、wmr |
esmodules | 始終由支援的工具設定。 | wmr |
類型 | 要求來自有興趣於類型宣告的 TypeScript。 |
(1) import
和 require
都獨立於參考語法設定。require
永遠優先度較低。
下列語法將設定 import
條件
import
宣告import()
表達式<script type="module">
<link rel="preload/prefetch">
new Worker(..., { type: "module" })
import
區段import.hot.accept/decline([...])
Worklet.addModule
下列語法將設定 require
條件
require(...)
define()
require([...])
require.resolve()
require.ensure([...])
require.context
module.hot.accept/decline([...])
<script src="...">
下列語法將設定 style
條件
@import
<link rel="stylesheet">
下列語法將設定 asset
條件
url()
new URL(..., import.meta.url)
<img src="...">
下列語法將設定 script
條件
<script src="...">
script
應僅在不支援模組系統時設定。當腳本由支援 CommonJs 的系統預處理時,應改為設定 require
。
在尋找可注入 HTML 頁面中為腳本標籤的 javascript 檔案時,應使用此條件,且無需額外的預處理。
下列條件設定為各種最佳化
條件 | 說明 | 支援 |
---|---|---|
production | 在生產環境中。 不應包含任何開發工具。 | webpack |
development | 在開發環境中。 應包含開發工具。 | webpack |
注意:由於並非所有人都支援 production
和 development
,因此在未設定任何條件時,不應做出任何假設。
下列條件會根據目標環境設定
條件 | 說明 | 支援 |
---|---|---|
browser | 程式碼將在瀏覽器中執行。 | webpack、esinstall、wmr |
electron | 程式碼將在 electron 中執行。(1) | webpack |
worker | 程式碼將在 (Web)Worker 中執行。(1) | webpack |
worklet | 程式碼將在 Worklet 中執行。(1) | - |
節點 | 程式碼將在 Node.js 中執行。 | Node.js、webpack、wmr(2) |
deno | 程式碼將在 Deno 中執行。 | - |
react-native | 程式碼將在 react-native 中執行。 | - |
(1) electron
、worker
和 worklet
會與 node
或 browser
結合使用,具體取決於情境。
(2) 這設定為瀏覽器目標環境。
由於每個環境都有多個版本,因此套用下列準則
node
:請參閱 engines
欄位以取得相容性。browser
:與當前規格和封裝發布時的第 4 階段提案相容。消費者端必須處理多重填補或轉譯。deno
:待定react-native
:待定下列條件會根據使用哪個工具預處理原始碼而設定。
條件 | 說明 | 支援 |
---|---|---|
webpack | 由 webpack 處理。 | webpack |
遺憾的是,沒有針對 Node.js 作為執行時期的 node-js
條件。這會簡化為 Node.js 建立例外。
下列工具支援自訂條件
工具 | 支援 | 備註 |
---|---|---|
Node.js | 是 | 使用 --conditions CLI 參數。 |
webpack | 是 | 使用 resolve.conditionNames 設定選項。 |
rollup | 是 | 針對 @rollup/plugin-node-resolve 使用 exportConditions 選項 |
esinstall | 否 | |
wmr | 否 |
對於自訂條件,建議使用下列命名結構
<公司名稱>:<條件名稱>
範例:example-corp:beta
、google:internal
。
所有模式都以單一 "."
項目說明套件,但也可以透過為每個項目重複模式,從多個項目進行延伸。
這些模式應當作為指南,而非嚴格的規則集。它們可以調整為個別套件。
這些模式基於下列目標/假設清單
exports
應撰寫為使用回退,以應對未知的未來案例。default
條件可以用於此。根據套件意圖,或許其他內容有意義,而這種情況下應當採用模式。範例:對於命令列工具,瀏覽器式的未來和回退沒有太多意義,而這種情況下應當改用類似 node.js 的環境和回退。
對於複雜的使用案例,需要透過嵌套這些條件來結合多個模式。
這些模式對於不使用環境特定 API 的套件有意義。
{
"type": "module",
"exports": "./index.js"
}
注意:僅提供 ESM 會對 node.js 造成限制。此類套件僅適用於 Node.js >= 14,且僅在使用 import
時適用。它無法與 require()
搭配使用。
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
大多數工具都會取得 ESM 版本。Node.js 在此處例外。它在使用 require()
時會取得 CommonJs 版本。這會導致使用 require()
和 import
參照此套件時產生兩個執行個體,但這並無妨礙,因為套件沒有狀態。
module
條件用於在使用支援 require()
的 ESM 工具(例如在為 Node.js 進行套件時使用的套件組建器)預處理目標為節點的程式碼時進行最佳化。對於此類工具,此例外會被略過。技術上來說這是可選的,但否則套件組建器會包含套件原始碼兩次。
如果您能夠將套件狀態隔離在 JSON 檔案中,您也可以使用無狀態模式。JSON 可以從 CommonJs 和 ESM 使用,而不會使用其他模組系統污染圖形。
請注意,這裡的無狀態也表示類別執行個體不會使用 instanceof
進行測試,因為由於模組重複執行個體化,因此可能會有兩個不同的類別。
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
// wrapper.js
import cjs from './index.cjs';
export const A = cjs.A;
export const B = cjs.B;
在有狀態套件中,我們必須確保套件不會執行個體化兩次。
這對大多數工具來說都不是問題,但 Node.js 在此處再次例外。對於 Node.js,我們始終使用 CommonJs 版本,並使用 ESM 包裝器在 ESM 中公開命名匯出。
我們再次使用 module
條件進行最佳化。
{
"type": "commonjs",
"exports": "./index.js"
}
提供 "type": "commonjs"
有助於靜態偵測 CommonJs 檔案。
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
請注意,儘管對 dist-bundle.js
使用 "type": "module"
和 .js
,但此檔案並非 ESM 格式。它應使用全域變數,以允許直接使用為腳本標籤。
當套件包含兩個版本時,這些模式才有意義,一個版本用於開發,另一個版本用於生產。例如,開發版本可以包含額外程式碼,以提供更好的錯誤訊息或額外的警告。
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
當支援 development
條件時,我們使用增強的開發版本。否則,在生產環境中或模式未知時,我們使用最佳化版本。
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"node": "./wrapper-process-env.cjs",
"default": "./index-optimized.js"
}
}
// wrapper-process-env.cjs
if (process.env.NODE_ENV !== 'development') {
module.exports = require('./index-optimized.cjs');
} else {
module.exports = require('./index-with-devtools.cjs');
}
我們偏好透過 production
或 development
條件靜態偵測生產/開發模式。
Node.js 允許透過 process.env.NODE_ENV
在執行時間偵測生產/開發模式,因此我們在 Node.js 中將其用作備用。同步條件匯入 ESM 不可能,而且我們不想載入套件兩次,因此我們必須對執行時間偵測使用 CommonJs。
當無法偵測模式時,我們會退回到生產版本。
應選擇一個備用環境,讓套件支援未來的環境有意義。一般來說,應假設類似的瀏覽器環境。
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
這是針對一個套件的範例,其中包含生產和開發使用的最佳化,並針對 process.env
進行執行時期偵測,同時也提供 CommonJs 和 ESM 版本
{
"type": "module",
"exports": {
"node": {
"development": {
"module": "./index-with-devtools.js",
"import": "./wrapper-with-devtools.js",
"require": "./index-with-devtools.cjs"
},
"production": {
"module": "./index-optimized.js",
"import": "./wrapper-optimized.js",
"require": "./index-optimized.cjs"
},
"default": "./wrapper-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
這是針對一個套件的範例,其中支援 Node.js、瀏覽器和 Electron,並包含生產和開發使用的最佳化,並針對 process.env
進行執行時期偵測,同時也提供 CommonJs 和 ESM 版本。
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
看起來很複雜,對吧。我們已經能夠降低一些複雜性,因為我們可以做出一個假設:只有 node
需要 CommonJs 版本,並且可以使用 process.env
偵測生產/開發。
default
匯出。不同工具處理方式不同。只使用命名匯出。.cjs
或 type: "commonjs"
,以清楚標記原始程式碼為 CommonJs。這使得工具可以靜態偵測是否使用 CommonJs 或 ESM。這對於僅支援 ESM 而非 CommonJs 的工具非常重要。data:
url 要求。