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