SAP Spartacus 的延遲載入 Lazy load 設計原理
延遲載入,也稱為程式碼拆分,可讓您將 JavaScript 程式碼分成多個塊。 結果是當用戶訪問第一頁時,您不必載入完整應用程式的所有 JavaScript。 相反,只加載給定頁面所需的塊。 在導航到其他頁面時,會在需要時載入其他塊。
這種方法可以顯著改善“互動時間”,尤其是在低端移動裝置訪問複雜 Web 應用程式的情況下。
Spartacus Approach to Lazy Loading
程式碼拆分是一種必須在應用程式構建時完成的技術。 Angular 提供的程式碼拆分通常是基於路由的,這意味著著陸頁有一個塊,產品頁面有另一個塊,依此類推。
由於 Spartacus 主要是 CMS 驅動的,因此無法在構建時決定每個路由的實際應用邏輯。業務使用者最終將通過引入或刪除元件來改變頁面結構。 這就是為什麼需要另一種延遲載入方法的原因,Spartacus 通過以下方式提供:
- CMS 元件的延遲載入
- CMS 驅動的功能模組延遲載入 - CMS-driven lazy loading of feature modules
Defining Dynamic Imports Only in the Main Application
動態匯入是一種用於促進延遲載入並允許程式碼拆分的技術,只能在主應用程式 - main application 中使用。 無法在預構建庫中定義動態匯入。
這是一個不幸的限制,導致必須由客戶新增一些應用程式程式碼。 儘管自定義程式碼的數量被限制在最低限度,但我們將在未來版本的 schematics library 中新增一項功能,以自動新增延遲載入模組。
Avoiding Static Imports for Lazy-Loaded Code
為了使程式碼拆分成為可能,您的靜態 JavaScript 程式碼(主應用程式包)不應該對您想要延遲載入的程式碼進行任何靜態匯入。 如果你真的這麼做了,構建器會注意到程式碼已經包含在內,因此不會為其生成單獨的塊。 這在從庫中匯入符號的情況下尤其重要。
在撰寫本文時(Angular 9 和 Angular 10),將靜態匯入與動態匯入混合用於相同的庫入口點,即使對於不同的符號,也會破壞該庫入口點的延遲載入和 tree shaking. 如果您要這樣做,它將在構建中靜態地包含整個入口點。 因此,強烈建議您為必須靜態載入的程式碼建立特定的入口點,併為可以延遲載入的程式碼建立單獨的入口點。
Configuration in Lazy-Loaded Modules
如果延遲載入模組內部提供了額外的配置,Spartacus 會將其合併到全域性應用程式配置中,以支援現有元件和服務的延遲載入場景。 在大多數情況下,尤其是當延遲載入的模組主要提供預設配置時,這可以可靠地工作。 但是,如果過度使用它會導致問題,尤其是當兩個模組為配置的同一部分提供不同的配置時。 可以通過在主應用程式中提供必要的覆蓋來修復諸如此類的場景。
這種合併功能是通過預設啟用的相容性機制實現的,但您可以使用 disableConfigUpdates 功能標誌禁用它。 如果您正在開發必須從延遲載入的模組中掛鉤到配置的新模組,則應改用 ConfigurationService.unifiedConfig$。 此功能在下一節中描述。
Unified Configuration
統一配置提供了一種獲取全域性配置的方法,該配置包括根配置和來自已載入延遲載入模組的配置。
ConfigurationService.unifiedConfig$ 將統一配置公開為每次更改時發出新配置的 observable。 例如,每次載入和例項化具有提供的配置的延遲載入模組時都會發生這種情況。
所有配置部分都按照嚴格的順序合併,實際配置始終覆蓋預設配置,並且根模組(即app shell)中定義的配置具有優先權。
以下示例顯示了根應用程式和兩個延遲載入模組中提供的不同配置的合併順序,其中列表中的每個後續項都可以覆蓋前一項:
- 預設根配置
- 延遲載入模組 1 的預設配置
- 延遲載入模組 2 的預設配置
- 延遲載入模組 1 的配置
- 延遲載入模組 2 的配置
- 根配置(始終優先)
Providers in Lazy-Loaded Modules
延遲載入模組中提供的注入令牌對根應用程式中提供的服務不可見。 這尤其適用於多提供的令牌,例如 HttpInterceptors、各種處理程式等。
為了減輕這個缺點,一些 Spartacus 功能,例如 PageMetaService(使用 PageMetaResolver 令牌)或 ConverterService(主要使用介面卡序列化器和規範化器),後臺使用統一注入器。 通過這樣做,他們可以訪問延遲載入的令牌,並可以利用它們來實現全域性功能。
對於不依賴於統一注入器的機制(例如,來自大多數非 Spartacus 庫的功能,例如核心 Angular 庫),建議您始終使用這些型別的令牌預先載入模組。
unified injector
統一注入器提供了一種注入令牌或多提供令牌的方法,同時考慮到根注入器和來自延遲載入功能的注入器。 注入器公開一個可觀察的物件,每次統一注入器的狀態發生變化時,該觀察物件都會為指定的令牌發出一組新的可注入物件。
Avoiding Importing the HttpClientModule in Your Lazy-Loaded Modules
一般來說,HttpClientModule 應該在根應用程式中匯入,而不是在庫中。 例如,如果您將它匯入到延遲載入的庫中,則根庫中的所有注入器對於源自延遲載入模組的 HTTP 呼叫都是不可見的。
雖然技術上可以在庫中匯入 HttpClientModule ,但在大多數情況下這不是預期的,並且可能導致難以解釋的錯誤,因此請記住這一點。
Lazy Loading of CMS Components
Configuration of Lazy Loading CMS Components
CMS 程式碼的延遲載入是通過在 CMS 對映配置中指定動態匯入代替靜態引用的元件類來實現的。 下面是一個例子:
{
cmsComponents: {
SimpleResponsiveBannerComponent: {
component: () => import('./lazy/lazy.component').then(m => m.LazyComponent)
}
}
}
Technical Details
CMS 元件對映中對動態匯入的支援是使用可定製的元件處理程式(特別是 LazyComponentHandler)實現的。
可以擴充套件此處理程式以自定義其行為、新增特殊鉤子或不同的觸發器,或者實現可以選擇性地重用現有處理程式的全新處理程式。
Lazy Loading of Modules
- 懶載入不僅是元件程式碼,還有核心部分(包括NgRx狀態)
- 在第一次需要時只加載一次功能
- 提供共享的、延遲載入的依賴模組
- 當實現被相關功能配置覆蓋時,CMS 請求元件會觸發功能模組的延遲載入。
Configuration of Lazy-Loaded Modules
-
功能模組的動態匯入(必須在主應用程式中定義)
-
有關特定功能涵蓋哪些 CMS 元件的資訊(可以成為庫的一部分並靜態匯入)。 此資訊以 cmsComponents 鍵下的字串陣列的形式提供。 下面是一個例子:
{
featureModules: {
organization: {
module: () =>
import('@spartacus/my-account/organization').then(
(m) => m.OrganizationModule
),
cmsComponents: [
'OrderApprovalListComponent',
'ManageBudgetsListComponent',
'ManageCostCentersListComponent',
'ManagePermissionsListComponent',
'ManageUnitsListComponent',
'ManageUserGroupsListComponent',
'ManageUsersListComponent',
],
},
},
}
例子:
[圖片上傳中...(image.png-c043ed-1625895062179-0)]
Component Mapping Configuration in Lazy-Loaded Modules
延遲載入模組中的預設 CMS 對映配置應該以與靜態匯入模組完全相同的方式定義。
Spartacus 能夠從延遲載入功能中提取 CMS 元件對映配置,並使用它來解析該功能所涵蓋的元件類和工廠。 這就是為什麼可以並推薦使用在延遲載入模組中提供預設 CMS 對映配置的標準方式的原因。 因此,完全相同的模組和庫入口點可以根據需要動態或靜態匯入,並且仍然可以通過在應用程式中提供配置覆蓋來從應用程式級別覆蓋延遲載入的 CMS 配置。
Defining Shared-Dependency Modules
通過在功能配置的依賴項屬性中提供動態匯入陣列,可以將一些邏輯提取到共享的延遲載入模組中,該模組可以定義為功能模組的延遲載入依賴項。 下面是一個例子:
{
featureModules: {
organization: {
module: () =>
import('@spartacus/my-account/organization').then(
(m) => m.OrganizationModule
),
dependencies: [
() =>
import('@spartacus/storefinder/core').then(
(m) => m.OrganizationModule
),
// ,,
],
},
},
}
當延遲載入依賴它的第一個特性時,這種未命名的依賴模組只會被例項化一次。 它的提供者為傳遞給特徵模組的組合注入器做出貢獻,因此,所有特徵服務和元件都可以訪問依賴模組提供的服務。
Combined Injector
任何延遲載入的模組都可以從根應用程式注入器和依賴模組注入器注入(即可以訪問)服務和令牌。 這是可能的,因為每次例項化具有依賴項的功能模組時都會建立 CombinedInjector。
當一個被延遲載入模組覆蓋的 CMS 元件被例項化時,它可以注入(即訪問)以下服務:
- ModuleInjector 層次結構,從功能模組注入器開始,包括依賴模組和根注入器
- ElementInjector 層次結構,在每個 DOM 元素上隱式建立
Initializing Lazy Loaded Modules
Spartacus 提供了一個 MODULE_INITIALIZER 來代替 Angular APP_INITIALIZER 來初始化延遲載入的模組。
APP_INITIALIZER 機制在任何延遲載入發生之前完成應用程式的初始化,因此在載入時可能需要執行初始化邏輯的延遲載入功能無法這樣做。
MODULE_INITIALIZER 注入令牌可用於在旨在延遲載入的模組中提供初始化函式。 MODULE_INITIALIZER 由 Spartacus 延遲載入機制支援,因此,使用 MODULE_INITIALIZER 提供的初始化函式將在它們定義的模組被延遲載入之前執行。
您可以像配置 APP_INITIALIZER 一樣配置 MODULE_INITIALIZER。 下面是一個例子:
...
import { MODULE_INITIALIZER } from '@spartacus/core';
...
export function myFactoryFunction(
dependencyOne: DependencyOne
) {
const result = () => {
// add initialization logic here
};
return result;
}
@NgModule({
providers: [
{
provide: MODULE_INITIALIZER,
useFactory: myFactoryFunction,
deps: [DependencyOne],
multi: true,
},
],
})
export class MyLazyLoadedModule {}
Preparing Libraries to Work with Lazy Loading
Providing Fine-Grained Entry Points in Your Library
在您的庫中提供細粒度的入口點。
從相同的入口點混合靜態和動態匯入會破壞延遲載入並影響 tree-shaking,因此任何將直接用於動態匯入的庫都應該公開細粒度的輔助入口點以優化程式碼拆分。
作為慣例,Spartacus 暴露功能的根入口點,例如@spartacus/orgainzation/administration/root。 這種型別的入口點包含所有不應或不能延遲載入的程式碼。 來自根入口點的模組應該在根應用程式中靜態匯入,這意味著它們將被預先載入並在主應用程式塊中可用。
Separating Static Code from Lazy-Loaded Code
當您使用 Angular Dependency Injection 時,注入器中的提供程式列表在注入器初始化後不應更改。這種正規化特別適用於任何多提供的令牌、處理程式,尤其適用於任何 Angular 原生多提供的令牌,例如 HTTP_INTERCEPTOR、APP_INITIALIZER 等。
結果是延遲載入模組中的任何多提供令牌對於根或其他延遲載入塊中提供的模組和服務將不可見,但使用 unified injector 注入的多提供令牌除外。
一些 Spartacus 功能,例如 PageMetaService 或 ConverterService,使用 UnifiedInjector 來了解可以延遲載入的令牌,以便全域性邏輯(例如 SEO 功能)即使邏輯延遲載入該功能也能可靠地工作。例如,商店定位器頁面元解析器可以在使用商店定位器功能的前提下,被延遲載入。
Spartacus 配置也是通過提供配置塊來定義的,由於相容機制將配置從延遲載入功能貢獻到全域性配置,因此處理方式略有不同。這種機制可以通過特性標誌禁用,將來會預設關閉,以支援統一配置特性。
如果根服務無法看到延遲載入提供程式的問題,則始終可以將此類程式碼包含在預先可用的靜態連結模組中。建議在您的庫中建立一個單獨的入口點(按照慣例,命名為 root,例如 my-library/root),其中包含最少的程式碼,將包含在主包中,並且從一開始就可用。