本文檔描述了 Angular 包格式 (APF)。APF 是針對(duì) npm 包結(jié)構(gòu)和格式的 Angular 專用規(guī)范,所有第一方 Angular 包(?@angular/core
? 、 ?@angular/material
? 等)和大多數(shù)第三方 Angular 庫(kù)都使用了該規(guī)范。
APF 能讓包在使用 Angular 的大多數(shù)常見(jiàn)場(chǎng)景下無(wú)縫工作。使用 APF 的包與 Angular 團(tuán)隊(duì)提供的工具以及更廣泛的 JavaScript 生態(tài)系統(tǒng)兼容。建議第三方庫(kù)開(kāi)發(fā)者也都遵循這種格式。
APF 與 Angular 的其余部分一起進(jìn)行版本控制,每個(gè)主要版本都改進(jìn)了包格式。你可以在此 google doc 中找到 v13 之前版本的規(guī)范。
在當(dāng)今的 JavaScript 環(huán)境中,開(kāi)發(fā)人員將使用多種不同的工具鏈(Webpack、rollup、esbuild 等)以多種不同的方式使用包。這些工具可能理解并需要不同的輸入 —— 一些工具能處理最新的 ES 語(yǔ)言版本,而其他工具也許要直接使用較舊的 ES 版本。
這種 Angular 分發(fā)格式支持所有常用的開(kāi)發(fā)工具和工作流,并著重于優(yōu)化,從而縮小應(yīng)用程序有效負(fù)載大小或縮短開(kāi)發(fā)迭代周期(構(gòu)建時(shí)間)。
開(kāi)發(fā)人員可以依靠 Angular CLI 和 ng-packagr(Angular CLI 使用的構(gòu)建工具)來(lái)生成 APF 格式的包。
以下示例顯示了 ?@angular/core
? 包文件布局的簡(jiǎn)化版本,并附有對(duì)包中每個(gè)文件的解釋。
此表描述了 ?node_modules/@angular/core
? 下的文件布局,注釋為描述文件和目錄的用途:
文件 |
用途 |
---|---|
README.md
|
包 README,由 npmjs web UI 使用。 |
package.json
|
主要的 |
index.d.ts
|
主入口點(diǎn) |
|
未展平的 ES2020 格式的 |
esm2020/testing/
|
未扁平化的 ES2020 格式的 |
fesm2015/
─ core.mjs
─ core.mjs.map
─ testing.mjs
─ testing.mjs.map
|
扁平化 (FESM) ES2015 格式的所有入口點(diǎn)的代碼,以及源碼映射。 |
fesm2020/
─ core.mjs
─ core.mjs.map
─ testing.mjs
─ testing.mjs.map
|
扁平化 (FESM) ES2020 格式的所有入口點(diǎn)的代碼,以及源碼映射。 |
testing/
|
代表 |
testing/index.d.ts
|
為 |
主 ?package.json
? 包含重要的包元數(shù)據(jù),包括以下內(nèi)容:
"exports"
? 字段,用于定義所有入口點(diǎn)的可用源碼格式@angular/core
? 的可用源碼格式的一些鍵,供不理解 ?"exports"
? 的工具使用。這些鍵已棄用,隨著對(duì) ?"exports"
? 的支持在整個(gè)生態(tài)系統(tǒng)中逐步退出,這些鍵將被刪除。頂級(jí) ?package.json
? 包含此鍵:
{
"type": "module"
}
這會(huì)通知解析器,此包中的代碼正在使用 EcmaScript 模塊而不是 CommonJS 模塊。
?"exports"
? 字段具有以下結(jié)構(gòu):
"exports": {
"./schematics/*": {
"default": "./schematics/*.js"
},
"./package.json": {
"default": "./package.json"
},
".": {
"types": "./core.d.ts",
"esm2020": "./esm2020/core.mjs",
"es2020": "./fesm2020/core.mjs",
"es2015": "./fesm2015/core.mjs",
"node": "./fesm2015/core.mjs",
"default": "./fesm2020/core.mjs"
},
"./testing": {
"types": "./testing/testing.d.ts",
"esm2020": "./esm2020/testing/testing.mjs",
"es2020": "./fesm2020/testing.mjs",
"es2015": "./fesm2015/testing.mjs",
"node": "./fesm2015/testing.mjs",
"default": "./fesm2020/testing.mjs"
}
}
主要看 "."
和 "./testing"
這兩個(gè)鍵,它們分別定義了 @angular/core
主要入口點(diǎn)和 @angular/core/testing
次要入口點(diǎn)的可用代碼格式。對(duì)于每個(gè)入口點(diǎn),可用的格式為:
格式 |
詳情 |
---|---|
類型定義( |
TypeScript 在依賴于給定包時(shí)使用 |
es2020
|
已展平為單個(gè)源文件的 ES2020 代碼。 |
es2015
|
已展平為單個(gè)源文件的 ES2015 代碼。 |
esm2020
|
未展平的源文件中的 ES2020 代碼 |
認(rèn)識(shí)這些鍵的工具可以優(yōu)先從 ?"exports"
? 中選擇所需的代碼格式。其余 2 個(gè)鍵控制工具的默認(rèn)行為:
"node"
? 在 Node.js 中加載包時(shí)選擇扁平化的 ES2015 代碼。使用這種格式是由于 ?zone.js
? 的要求,因?yàn)樗恢С衷?nbsp;?async
?/ ?await
?ES2017 語(yǔ)法。因此,指示 Node 使用 ES2015 代碼,其中 ?async
?/ ?await
?結(jié)構(gòu)已降級(jí)為 Promises。
"default"
? 為所有其他消費(fèi)者選擇扁平化的 ES2020 代碼。庫(kù)可能希望公開(kāi)其他靜態(tài)文件,這些文件沒(méi)有被基于 JavaScript 的入口點(diǎn)(比如 Sass mixins 或預(yù)編譯的 CSS)的導(dǎo)出所捕獲。
除了 ?"exports"
? 之外,頂級(jí) ?package.json
? 還為不支持 ?"exports"
? 的解析器定義了舊模塊解析鍵。對(duì)于 ?@angular/core
?,這些是:
{
"fesm2020": "./fesm2020/core.mjs",
"fesm2015": "./fesm2015/core.mjs",
"esm2020": "./esm2020/core.mjs",
"typings": "./core.d.ts",
"module": "./fesm2015/core.mjs",
"es2020": "./fesm2020/core.mjs",
}
如上,模塊解析器可以用這些鍵來(lái)加載特定的代碼格式。
注意:
與 ?"default"
? 不同,?"module"
? 是為 Node 以及任何未配置為使用特定鍵的工具選擇的格式。它基本和 ?"node"
? 一樣,但由于 ZoneJS 的限制,選擇了 ES2015 代碼。
?package.json
? 的最后一個(gè)功能是聲明此包是否有副作用。
{
"sideEffects": false
}
大多數(shù) Angular 包不應(yīng)該依賴于頂級(jí)副作用,因此應(yīng)該包含這個(gè)聲明。
APF 中的包,包含一個(gè)主要入口點(diǎn)和零到多個(gè)次要入口點(diǎn)(比如 ?@angular/common/http
?)。入口點(diǎn)有多種功能。
@angular/core
? 和 ?@angular/core/testing
?)。用戶通常將這些入口點(diǎn)視為具有不同用途或功能的不同符號(hào)組。
特定入口點(diǎn)可能僅用于特殊目的,比如測(cè)試。此類 API 可以與主入口點(diǎn)分離,以減少它們被意外或錯(cuò)誤使用的機(jī)會(huì)。
許多現(xiàn)代構(gòu)建工具只能在 ES 模塊級(jí)別進(jìn)行“代碼拆分”(又名惰性加載)。由于 APF 主要為每個(gè)入口點(diǎn)使用一個(gè)“扁平” ES 模塊,這意味著大多數(shù)構(gòu)建工具無(wú)法將單個(gè)入口點(diǎn)中的代碼拆分為多個(gè)輸出塊。
APF 包的一般規(guī)則是為盡可能小的邏輯相關(guān)代碼集使用入口點(diǎn)。比如,Angular Material 包將每個(gè)邏輯組件或一組組件作為單獨(dú)的入口點(diǎn)發(fā)布 - 一個(gè)用于按鈕,一個(gè)用于選項(xiàng)卡等。如果需要,這允許單獨(dú)惰性加載每個(gè) Material 組件。
并非所有庫(kù)都需要這樣的粒度。大多數(shù)具有單一邏輯目的的庫(kù)應(yīng)該作為單一入口點(diǎn)發(fā)布。比如 ?@angular/core
? 為運(yùn)行時(shí)使用單個(gè)入口點(diǎn),因?yàn)?nbsp;Angular 運(yùn)行時(shí)通常用作單個(gè)實(shí)體。
可以通過(guò)包的 ?package.json
? 的 ?"exports"
? 字段解析輔助入口點(diǎn)。
markdown 格式的自述文件,用于在 npm 和 github 上顯示包的描述。
?@angular/core
? 包的示例自述內(nèi)容:
Angular
=======
The sources for this package are in the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests against that repo.
License: MIT
APF 格式的庫(kù)必須以“部分編譯”模式發(fā)布。這是 ?ngc
?的一種編譯模式,它生成不依賴于特定 Angular 運(yùn)行時(shí)版本的已編譯 Angular 代碼,與用于應(yīng)用程序的完整編譯形成對(duì)比,其中 Angular 編譯器和運(yùn)行時(shí)版本必須完全匹配。
要部分編譯 Angular 代碼,請(qǐng)?jiān)?nbsp;?tsconfig.json
? 中的 ?"angularCompilerOptions"
? 中使用 ?"compilationMode"
? 標(biāo)志:
{
…
"angularCompilerOptions": {
"compilationMode": "partial",
}
}
然后,在應(yīng)用程序構(gòu)建過(guò)程中,Angular CLI 將部分編譯的庫(kù)代碼轉(zhuǎn)換為完全編譯的代碼。
APF 指定代碼要以“扁平化”的 ES 模塊格式發(fā)布。這顯著減少了 Angular 應(yīng)用程序的構(gòu)建時(shí)間以及最終應(yīng)用程序包的下載和解析時(shí)間。請(qǐng)查看 Nolan Lawson 發(fā)表的優(yōu)秀文章“小模塊的成本”。
Angular 編譯器支持生成索引 ES 模塊文件,然后可以讓這些文件借助 Rollup 等工具生成扁平化模塊,從而生成我們稱為扁平化 ES 模塊或 FESM 的文件格式。
FESM 是一種文件格式,它會(huì)將所有可從入口點(diǎn)訪問(wèn)的 ES 模塊扁平化為單個(gè) ES 模塊。它是通過(guò)跟蹤包中的所有導(dǎo)入并將該代碼復(fù)制到單個(gè)文件中而生成的,同時(shí)保留所有公共 ES 導(dǎo)出并刪除所有私有導(dǎo)入。
縮寫(xiě)名稱“FESM”(發(fā)音為“phesom”)后面可以有一個(gè)數(shù)字,比如“FESM5”或“FESM2015”。數(shù)字是指模塊內(nèi) JavaScript 的語(yǔ)言級(jí)別。所以 FESM5 文件將是 ESM+ES5(導(dǎo)入/導(dǎo)出語(yǔ)句和 ES5 源代碼)。
要生成扁平化的 ES 模塊索引文件,請(qǐng)?jiān)?tsconfig.json 文件中使用以下配置選項(xiàng):
{
"compilerOptions": {
…
"module": "esnext",
"target": "es2020",
…
},
"angularCompilerOptions": {
…
"flatModuleOutFile": "my-ui-lib.js",
"flatModuleId": "my-ui-lib"
}
}
一旦 ngc 生成了索引文件(比如 ?my-ui-lib.js
?),打包器和優(yōu)化器(如 Rollup)就可用于生成扁平化的 ESM 文件。
從 webpack v4 開(kāi)始,對(duì)于 webpack 用戶來(lái)說(shuō),ES 模塊優(yōu)化的扁平化應(yīng)該不是必需的,其實(shí)理論上我們應(yīng)該能夠在不扁平化 webpack 中的模塊的情況下獲得更好的代碼拆分。在實(shí)踐中,當(dāng)使用非扁平化模塊作為 webpack v4 的輸入時(shí),我們?nèi)匀粫?huì)看到大小增加了。這就是為什么 package.json 中的 ?"module"
? 和 ?"es2020"
? 條目仍然指向 fesm 文件的原因。我們正在調(diào)查此問(wèn)題,并希望在解決大小回歸問(wèn)題后將 package.json 中的 ?"module"
? 和 ?"es2020"
? 入口點(diǎn)切換到未扁平化的文件。APF 目前包含未扁平化的 ESM2020 代碼,目的是驗(yàn)證此類未來(lái)的更改。
默認(rèn)情況下,EcmaScript 模塊是有副作用的:從模塊導(dǎo)入可確保該模塊頂層的任何代碼都將執(zhí)行。這通常是不可取的,因?yàn)榈湫湍K中的大多數(shù)副作用代碼并不是真正的副作用,而是僅影響特定符號(hào)。如果沒(méi)有導(dǎo)入和使用這些符號(hào),通常需要在稱為 tree-shaking 的優(yōu)化過(guò)程中將它們刪除,而副作用代碼可以防止這種情況發(fā)生。
諸如 Webpack 之類的構(gòu)建工具支持一個(gè)標(biāo)志,該標(biāo)志允許包聲明它們并不依賴于其模塊頂層的副作用代碼,從而使工具可以更自由地對(duì)包中的代碼進(jìn)行搖樹(shù)優(yōu)化。這些優(yōu)化的最終結(jié)果應(yīng)該是較小的包大小和代碼拆分后包塊中更好的代碼分布。如果此優(yōu)化包含非本地副作用,則此優(yōu)化可能會(huì)破壞你的代碼 - 然而,這在 Angular 應(yīng)用程序中并不常見(jiàn),并且通常是糟糕設(shè)計(jì)的標(biāo)志。我們的建議是讓所有包通過(guò)將 sideEffects 屬性設(shè)置為 false 來(lái)聲明無(wú)副作用狀態(tài),并且讓開(kāi)發(fā)人員遵循 Angular 風(fēng)格指南,這自然會(huì)導(dǎo)致代碼沒(méi)有非本地副作用。
ES2020 語(yǔ)言級(jí)別現(xiàn)在是 Angular CLI 和其他工具使用的默認(rèn)語(yǔ)言級(jí)別。Angular CLI 會(huì)將捆綁包降級(jí)到所有目標(biāo)瀏覽器在應(yīng)用程序構(gòu)建時(shí)都支持的語(yǔ)言級(jí)別。
從 APF v8 開(kāi)始,我們現(xiàn)在更喜歡運(yùn)行 API Extractor 來(lái)打包 TypeScript 定義,以便整個(gè) API 都出現(xiàn)在單個(gè)文件中。
在之前的 APF 版本中,每個(gè)入口點(diǎn)都會(huì)在 .d.ts 入口點(diǎn)旁邊有一個(gè) ?src
?目錄,該目錄包含與原始源代碼結(jié)構(gòu)匹配的單個(gè) d.ts 文件。雖然這種分發(fā)格式仍然被允許和支持,但非常不鼓勵(lì)它,因?yàn)樗鼤?huì)弄暈 IDE 之類的工具,然后提供錯(cuò)誤的自動(dòng)完成,并允許用戶依賴深度導(dǎo)入的路徑,這些路徑通常不被認(rèn)為是庫(kù)或包的公共 API。
從 APF v10 開(kāi)始,我們建議添加 tslib 作為主要入口點(diǎn)的直接依賴項(xiàng)。這是因?yàn)?nbsp;tslib 版本與用來(lái)編譯庫(kù)的 TypeScript 版本相關(guān)聯(lián)。
本文檔中特意使用了以下術(shù)語(yǔ)。在本節(jié)中,我們定義所有這些以便更清晰。
發(fā)布到 NPM 并一起安裝的最小文件集,比如 ?@angular/core
?。該包中包含一個(gè)名為 package.json 的清單、編譯后的源代碼、TypeScript 定義文件、源碼映射、元數(shù)據(jù)等。該包是通過(guò) ?npm install @angular/core
? 安裝的。
包含在模塊中的類、函數(shù)、常量或變量,可選擇通過(guò)模塊導(dǎo)出,以便對(duì)外界可見(jiàn)。
ECMAScript 模塊的縮寫(xiě)。包含導(dǎo)入和導(dǎo)出符號(hào)的語(yǔ)句的文件。這與 ECMAScript 規(guī)范中模塊的定義相同。
ECMAScript 模塊的縮寫(xiě)(見(jiàn)上文)。
Flattened ES Modules 的縮寫(xiě),由一種文件格式組成,該文件格式是通過(guò)將所有可從入口點(diǎn)訪問(wèn)的 ES 模塊扁平化為單個(gè) ES 模塊而創(chuàng)建的。
導(dǎo)入語(yǔ)句中使用的模塊的標(biāo)識(shí)符(比如 ?@angular/core
?)。此 ID 通常直接映射到文件系統(tǒng)上的路徑,但由于有各種模塊解析策略,情況也并非總是如此。
模塊標(biāo)識(shí)符(見(jiàn)上文)。
用于將模塊 ID 轉(zhuǎn)換為文件系統(tǒng)路徑的算法。Node.js 就有一個(gè)良好定義且廣泛使用的,TypeScript 支持多種模塊解析策略,Closure Compiler 還有另一種策略。
模塊語(yǔ)法規(guī)范,至少涵蓋用于從文件導(dǎo)入和導(dǎo)出的語(yǔ)法。常見(jiàn)的模塊格式是 CommonJS(CJS,通常用于 Node.js 應(yīng)用程序)或 ECMAScript 模塊(ESM)。模塊格式僅表示單個(gè)模塊的封裝,而不表示用于構(gòu)成模塊內(nèi)容的 JavaScript 語(yǔ)言特性。正因?yàn)槿绱?,Angular 團(tuán)隊(duì)經(jīng)常使用語(yǔ)言級(jí)別說(shuō)明符作為模塊格式的后綴,比如 ESM+ES2015 指定模塊為 ESM 格式并包含降級(jí)到 ES2015 的代碼。
單個(gè) JS 文件形式的工件,由構(gòu)建工具(比如 Webpack或Rollup)生成,其中包含源自一個(gè)或多個(gè)模塊的符號(hào)。捆綁包是一種瀏覽器專用的解決方案,可減少瀏覽器開(kāi)始下載數(shù)百甚至數(shù)萬(wàn)個(gè)文件時(shí)可能造成的網(wǎng)絡(luò)壓力。Node.js 通常不使用捆綁包。常見(jiàn)的捆綁包格式是 UMD 和 System.register。
代碼的語(yǔ)言(ES2015 或 ES2020)。獨(dú)立于模塊格式。
旨在由用戶導(dǎo)入的模塊。它由唯一的模塊 ID 引用,并導(dǎo)出該模塊 ID 引用的公共 API。一個(gè)例子是 ?@angular/core
? 或 ?@angular/core/testing
?。?@angular/core
? 包中存在兩個(gè)入口點(diǎn),但它們導(dǎo)出不同的符號(hào)。一個(gè)包可以有許多入口點(diǎn)。
從不是入口點(diǎn)的模塊中檢索符號(hào)的過(guò)程。這些模塊 ID 通常被認(rèn)為是私有 API,它們可以在項(xiàng)目的生命周期內(nèi)或在創(chuàng)建給定包的捆綁包時(shí)更改。
來(lái)自入口點(diǎn)的導(dǎo)入。可用的頂級(jí)導(dǎo)入定義了公共 API,并在“@angular/name”模塊中公開(kāi),比如 ?@angular/core
? 或 ?@angular/common
?。
識(shí)別和刪除應(yīng)用程序中未使用的代碼的過(guò)程 - 也稱為死代碼消除。這是使用 Rollup 、 Closure Compiler 或 Terser 等工具在應(yīng)用程序級(jí)別執(zhí)行的全局優(yōu)化。
Angular 的預(yù)先編譯器。
從 API Extractor 生成的捆綁 TypeScript 定義。
更多建議: