Angular 项目中的可摇树依赖 - Tree-shakable dependencies
Tree-shakable dependencies in Angular projects
Tree-shakable 依賴更容易推理和編譯成更小的包。
Angular 模塊 (NgModules) 曾經(jīng)是提供應(yīng)用程序范圍依賴項(例如常量、配置、函數(shù)和基于類的服務(wù))的主要方式。 從 Angular 版本 6 開始,我們可以創(chuàng)建可搖樹的依賴項,甚至可以忽略 Angular 模塊。
Angular module providers create hard dependencies
當(dāng)我們使用 NgModule 裝飾器工廠的 providers 選項提供依賴項時,Angular 模塊文件頂部的 import 語句引用了依賴項文件。
這意味著 Angular 模塊中提供的所有服務(wù)都成為包的一部分,即使是那些不被 declarable 或其他依賴項使用的服務(wù)。 讓我們稱這些為硬依賴,因為它們不能被我們的構(gòu)建過程搖樹。
相反,我們可以通過讓依賴文件引用 Angular 模塊文件來反轉(zhuǎn)依賴關(guān)系。 這意味著即使應(yīng)用程序?qū)肓?Angular 模塊,它也不會引用依賴項,直到它在例如組件中使用依賴項。
Providing singleton services
許多基于類的服務(wù)被稱為應(yīng)用程序范圍的單例服務(wù)——或者簡稱為單例服務(wù),因為我們很少在平臺注入器級別使用它們。
Pre-Angular 6 singleton service providers
在 Angular 版本 2 到 5 中,我們必須向 NgModule 的 providers 選項添加單例服務(wù)。 然后我們必須注意,只有急切加載的 Angular 模塊才會導(dǎo)入提供的 Angular 模塊——按照慣例,這是我們應(yīng)用程序的 CoreModule。
// pre-six-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core';@Injectable() export class PreSixSingletonService {constructor(private http: HttpClient) {} } // pre-six.module.ts import { NgModule } from '@angular/core';import { PreSixSingletonService } from './pre-six-singleton.service';@NgModule({providers: [PreSixSingletonService], }) export class PreSixModule {} // core.module.ts import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core';import { PreSixModule } from './pre-six.module.ts';@NgModule({imports: [HttpClientModule, PreSixModule], }) export class CoreModule {}以上是 Pre-Angular 6 singleton service.
如果我們在延遲加載的功能模塊中導(dǎo)入提供 Angular 的模塊,我們將獲得不同的服務(wù)實例。
Providing services in mixed Angular modules
當(dāng)在帶有可聲明的 Angular 模塊中提供服務(wù)時,我們應(yīng)該使用 forRoot 模式來表明它是一個混合的 Angular 模塊——它同時提供了可聲明和依賴項。
這很重要,因為在延遲加載的 Angular 模塊中導(dǎo)入具有依賴項提供程序的 Angular 模塊將為該模塊注入器創(chuàng)建新的服務(wù)實例。 即使已經(jīng)在根模塊注入器中創(chuàng)建了一個實例,也會發(fā)生這種情況。
// pre-six-mixed.module.ts import { ModuleWithProviders, NgModule } from '@angular/core';import { MyComponent } from './my.component'; import { PreSixSingletonService } from './pre-six-singleton.service';@NgModule({declarations: [MyComponent],exports: [MyComponent], }) export class PreSixMixedModule {static forRoot(): ModuleWithProviders {return {ngModule: PreSixMixedModule,providers: [PreSixSingletonService],};} }以上是 The forRoot pattern for singleton services.
靜態(tài) forRoot 方法用于我們的 CoreModule,它成為根模塊注入器的一部分。
Tree-shakable singleton service providers
幸運的是,Angular 6 向 Injectable 裝飾器工廠添加了 providedIn 選項。 這是聲明應(yīng)用程序范圍的單例服務(wù)的一種更簡單的方法。
// modern-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core';@Injectable({providedIn: 'root', }) export class ModernSingletonService {constructor(private http: HttpClient) {} }以上是 Modern singleton service.
單例服務(wù)是在第一次構(gòu)建依賴它的任何組件時創(chuàng)建的。
始終使用 Injectable 裝飾基于類的服務(wù)被認為是最佳實踐。 它配置 Angular 以通過服務(wù)構(gòu)造函數(shù)注入依賴項。
在 Angular 版本 6 之前,如果我們的服務(wù)沒有依賴項,則 Injectable 裝飾器在技術(shù)上是不必要的。 盡管如此,添加它仍然被認為是最佳實踐,以便我們在以后添加依賴項時不會忘記這樣做。
現(xiàn)在我們有了 providedIn 選項,我們還有另一個理由總是將 Injectable 裝飾器添加到我們的單例服務(wù)中。
這個經(jīng)驗法則的一個例外是,如果我們創(chuàng)建的服務(wù)總是打算由工廠提供者構(gòu)建(使用 useFactory 選項)。 如果是這種情況,我們不應(yīng)指示 Angular 將依賴項注入其構(gòu)造函數(shù)。
providedIn: ‘root’
該選項將在根模塊注入器中提供單例服務(wù)。 這是為引導(dǎo)的 Angular 模塊創(chuàng)建的注入器——按照慣例是 AppModule.事實上,這個注入器用于所有急切加載的 Angular 模塊。
或者,我們可以將 providedIn 選項引用到一個 Angular 模塊,這類似于我們過去對混合 Angular 模塊使用 forRoot 模式所做的事情,但有一些例外。
// modern-singleton.service.ts import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core';import { ModernMixedModule } from './modern-mixed.module';@Injectable({providedIn: ModernMixedModule, }) export class ModernSingletonService {constructor(private http: HttpClient) {} } // modern-mixed.module.ts import { NgModule } from '@angular/core';import { MyComponent } from './my.component';@NgModule({declarations: [MyComponent],exports: [MyComponent], }) export class ModernMixedModule {}單例服務(wù)的現(xiàn)代 forRoot 替代方案。
與 ‘root’ 選項值相比,使用此方法有兩個不同之處:
- 除非已導(dǎo)入提供的 Angular 模塊,否則無法注入單例服務(wù)。
- 由于單獨的模塊注入器,延遲加載的 Angular 模塊和 AppModule 會創(chuàng)建自己的實例。
Providing primitive values
假設(shè)我們的任務(wù)是向 Internet Explorer 11 用戶顯示棄用通知。 我們將創(chuàng)建一個 InjectionToken。
這允許我們將布爾標(biāo)志注入服務(wù)、組件等。 同時,我們只對每個模塊注入器評估一次 Internet Explorer 11 檢測表達式。 這意味著根模塊注入器一次,延遲加載模塊注入器一次。
在 Angular 版本 4 和 5 中,我們必須使用 Angular 模塊為注入令牌提供值。
首先新建一個 token 實例:
// is-internet-explorer.token.ts import { InjectionToken } from '@angular/core';export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag');然后新建一個 module,通過 factory 為該 token 指定運行時應(yīng)該注入什么樣的值:
// internet-explorer.module.ts import { NgModule } from '@angular/core';import { isInternetExplorer11Token } from './is-internet-explorer-11.token';@NgModule({providers: [{provide: isInternetExplorer11Token,useFactory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(navigator.userAgent),},], }) export class InternetExplorerModule {}以上是:Angular 4–5 dependency injection token with factory provider.
Angular 6 的改進:
從 Angular 版本 6 開始,我們可以將工廠傳遞給 InjectionToken 構(gòu)造函數(shù),從而不再需要 Angular 模塊。
// is-internet-explorer-11.token.ts import { InjectionToken } from '@angular/core';export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag', {factory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(navigator.userAgent),providedIn: 'root', });使用工廠提供程序時,providedIn 默認為“root”,但讓我們通過保留它來明確。 它也與使用 Injectable 裝飾器工廠聲明提供者的方式更加一致。
Value factories with dependencies
我們決定將 user agent 字符串提取到它自己的依賴注入令牌中,我們可以在多個地方使用它,并且每個模塊注入器只從瀏覽器讀取一次。
在 Angular 版本 4 和 5 中,我們必須使用 deps 選項(依賴項的縮寫)來聲明工廠依賴項。
// user-agent.token.ts import { InjectionToken } from '@angular/core';export const userAgentToken: InjectionToken<string> = new InjectionToken('User agent string'); // is-internet-explorer.token.ts import { InjectionToken } from '@angular/core';export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag'); // internet-explorer.module.ts,在一個 module 里同時提供兩個 token 的值 import { Inject, NgModule } from '@angular/core';import { isInternetExplorer11Token } from './is-internet-explorer.token'; import { userAgentToken } from './user-agent.token';@NgModule({providers: [{ provide: userAgentToken, useFactory: () => navigator.userAgent },{deps: [[new Inject(userAgentToken)]],provide: isInternetExplorer11Token,useFactory: (userAgent: string): boolean => /Trident\/7\.0.+rv:11\.0/.test(userAgent),},], }) export class InternetExplorerModule {}不幸的是,依賴注入令牌構(gòu)造函數(shù)目前不允許我們聲明工廠提供程序依賴項。 相反,我們必須使用來自@angular/core 的注入函數(shù)。
// user-agent.token.ts import { InjectionToken } from '@angular/core';export const userAgentToken: InjectionToken<string> = new InjectionToken('User agent string', {factory: (): string => navigator.userAgent,providedIn: 'root', }); // is-internet-explorer-11.token.ts import { inject, InjectionToken } from '@angular/core';import { userAgentToken } from './user-agent.token';export const isInternetExplorer11Token: InjectionToken<boolean> = new InjectionToken('Internet Explorer 11 flag', {factory: (): boolean => /Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)),providedIn: 'root', });以上是 Angular 6 之后,如何實例化具有依賴關(guān)系的 injection token 的代碼示例。
注入函數(shù)從提供它的模塊注入器中注入依賴項——在這個例子中是根模塊注入器。 它可以被 tree-shakable 提供者中的工廠使用。 Tree-shakable 基于類的服務(wù)也可以在它們的構(gòu)造函數(shù)和屬性初始化器中使用它。
Providing platform-specific APIs
為了利用特定于平臺的 API 并確保高水平的可測試性,我們可以使用依賴注入令牌來提供 API。
讓我們看一個 Location 的例子。 在瀏覽器中,它可用作全局變量 location,另外在 document.location 中。 它在 TypeScript 中具有 Location 類型。 如果你在你的一個服務(wù)中通過類型注入它,你可能沒有意識到 Location 是一個接口。
接口是 TypeScript 中的編譯時工件,Angular 無法將其用作依賴注入令牌。 Angular 在運行時解決依賴關(guān)系,因此我們必須使用在運行時可用的軟件工件。 很像 Map 或 WeakMap 的鍵。
相反,我們創(chuàng)建了一個依賴注入令牌并使用它來將 Location 注入到例如服務(wù)中。
// location.token.ts import { InjectionToken } from '@angular/core';export const locationToken: InjectionToken<Location> = new InjectionToken('Location API'); // browser.module.ts import { NgModule } from '@angular/core';import { locationToken } from './location.token';@NgModule({providers: [{ provide: locationToken, useFactory: (): Location => document.location }], }) export class BrowserModule {}以上是 Angular 4 - 5 的老式寫法。
Angular 6 的新式寫法:
// location.token.ts import { InjectionToken } from '@angular/core';export const locationToken: InjectionToken<Location> = new InjectionToken('Location API', {factory: (): Location => document.location,providedIn: 'root', });在 API 工廠中,我們使用全局變量 document. 這是在工廠中解析 Location API 的依賴項。 我們可以創(chuàng)建另一個依賴注入令牌,但事實證明 Angular 已經(jīng)為這個特定于平臺的 API 公開了一個——由@angular/common 包導(dǎo)出的 DOCUMENT 依賴注入令牌。
在 Angular 版本 4 和 5 中,我們將通過將其添加到 deps 選項來聲明工廠提供程序中的依賴項。
// location.token.ts import { InjectionToken } from '@angular/core';export const locationToken: InjectionToken<Location> = new InjectionToken('Location API'); // browser.module.ts import { DOCUMENT } from '@angular/common'; import { Inject, NgModule } from '@angular/core';import { locationToken } from './location.token';@NgModule({providers: [{deps: [[new Inject(DOCUMENT)]],provide: locationToken,useFactory: (document: Document): Location => document.location,},], }) export class BrowserModule {}下面是新式寫法:
和以前一樣,我們可以通過將工廠傳遞給依賴注入令牌構(gòu)造函數(shù)來擺脫 Angular 模塊。 請記住,我們必須將工廠依賴項轉(zhuǎn)換為對注入的調(diào)用。
// location.token.ts import { DOCUMENT } from '@angular/common'; import { inject, InjectionToken } from '@angular/core';export const locationToken: InjectionToken<Location> = new InjectionToken('Location API', {factory: (): Location => inject(DOCUMENT).location,providedIn: 'root', });現(xiàn)在我們有了一種為特定于平臺的 API 創(chuàng)建通用訪問器的方法。 這在測試依賴它們的 declarable 和服務(wù)時將證明是有用的。
Testing tree-shakable dependencies
在測試 tree-shakable 依賴項時,重要的是要注意依賴項默認由工廠提供,作為選項傳遞給 Injectable 和 InjectionToken。
為了覆蓋可搖樹依賴,我們使用 TestBed.overrideProvider,例如 TestBed.overrideProvider(userAgentToken, { useValue: ‘TestBrowser’ })。
Angular 模塊中的提供者僅在將 Angular 模塊添加到 Angular 測試模塊導(dǎo)入時才用于測試,例如 TestBed.configureTestingModule({imports: [InternetExplorerModule] })。
Do tree-shakable dependencies matter?
Tree-shakable 依賴對于小型應(yīng)用程序沒有多大意義,我們應(yīng)該能夠很容易地判斷一個服務(wù)是否在實際使用中。
相反,假設(shè)我們創(chuàng)建了一個供多個應(yīng)用程序使用的共享服務(wù)庫。 應(yīng)用程序包現(xiàn)在可以忽略在該特定應(yīng)用程序中未使用的服務(wù)。 這對于具有共享庫的 monorepo 工作區(qū)和 multirepo 項目都很有用。
Tree-shakable 依賴項對于 Angular 庫也很重要。 例如,假設(shè)我們在應(yīng)用程序中導(dǎo)入了所有 Angular Material 模塊,但僅使用了部分組件及其相關(guān)的基于類的服務(wù)。 因為 Angular Material 提供了搖樹服務(wù),所以我們的應(yīng)用程序包中只包含我們使用的服務(wù)。
Summary
我們已經(jīng)研究了使用 tree-shakable 提供程序配置注入器的現(xiàn)代選項。 與前 Angular 6 時代的提供者相比,可搖動樹的依賴項通常更容易推理且不易出錯。
來自共享庫和 Angular 庫的未使用的 tree-shakable 服務(wù)在編譯時被刪除,從而產(chǎn)生更小的包。
總結(jié)
以上是生活随笔為你收集整理的Angular 项目中的可摇树依赖 - Tree-shakable dependencies的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 特斯拉和福特不再提供车内 AM 收音机,
- 下一篇: 预算编制原则,分别代表了什么