1. 程式人生 > 其它 >Angular 專案中的可搖樹依賴 - Tree-shakable dependencies

Angular 專案中的可搖樹依賴 - Tree-shakable dependencies

Tree-shakable dependencies in Angular projects

Tree-shakable 依賴更容易推理和編譯成更小的包。

Angular 模組 (NgModules) 曾經是提供應用程式範圍依賴項(例如常量、配置、函式和基於類的服務)的主要方式。 從 Angular 版本 6 開始,我們可以建立可搖樹的依賴項,甚至可以忽略 Angular 模組。

Angular module providers create hard dependencies

當我們使用 NgModule 裝飾器工廠的 providers 選項提供依賴項時,Angular 模組檔案頂部的 import 語句引用了依賴項檔案。

這意味著 Angular 模組中提供的所有服務都成為包的一部分,即使是那些不被 declarable 或其他依賴項使用的服務。 讓我們稱這些為硬依賴,因為它們不能被我們的構建過程搖樹。

相反,我們可以通過讓依賴檔案引用 Angular 模組檔案來反轉依賴關係。 這意味著即使應用程式匯入了 Angular 模組,它也不會引用依賴項,直到它在例如元件中使用依賴項。

Providing singleton services

許多基於類的服務被稱為應用程式範圍的單例服務——或者簡稱為單例服務,因為我們很少在平臺注入器級別使用它們。

Pre-Angular 6 singleton service providers

在 Angular 版本 2 到 5 中,我們必須向 NgModule 的 providers 選項新增單例服務。 然後我們必須注意,只有急切載入的 Angular 模組才會匯入提供的 Angular 模組——按照慣例,這是我們應用程式的 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.

如果我們在延遲載入的功能模組中匯入提供 Angular 的模組,我們將獲得不同的服務例項。

Providing services in mixed Angular modules

當在帶有可宣告的 Angular 模組中提供服務時,我們應該使用 forRoot 模式來表明它是一個混合的 Angular 模組——它同時提供了可宣告和依賴項。

這很重要,因為在延遲載入的 Angular 模組中匯入具有依賴項提供程式的 Angular 模組將為該模組注入器建立新的服務例項。 即使已經在根模組注入器中建立了一個例項,也會發生這種情況。

// 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.

靜態 forRoot 方法用於我們的 CoreModule,它成為根模組注入器的一部分。

Tree-shakable singleton service providers

幸運的是,Angular 6 向 Injectable 裝飾器工廠添加了 providedIn 選項。 這是宣告應用程式範圍的單例服務的一種更簡單的方法。

// 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.

單例服務是在第一次構建依賴它的任何元件時建立的。

始終使用 Injectable 裝飾基於類的服務被認為是最佳實踐。 它配置 Angular 以通過服務建構函式注入依賴項。

在 Angular 版本 6 之前,如果我們的服務沒有依賴項,則 Injectable 裝飾器在技術上是不必要的。 儘管如此,新增它仍然被認為是最佳實踐,以便我們在以後新增依賴項時不會忘記這樣做。

現在我們有了 providedIn 選項,我們還有另一個理由總是將 Injectable 裝飾器新增到我們的單例服務中。

這個經驗法則的一個例外是,如果我們建立的服務總是打算由工廠提供者構建(使用 useFactory 選項)。 如果是這種情況,我們不應指示 Angular 將依賴項注入其建構函式。

providedIn: 'root'

該選項將在根模組注入器中提供單例服務。 這是為引導的 Angular 模組建立的注入器——按照慣例是 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 {}

單例服務的現代 forRoot 替代方案。

與 'root' 選項值相比,使用此方法有兩個不同之處:

  • 除非已匯入提供的 Angular 模組,否則無法注入單例服務。
  • 由於單獨的模組注入器,延遲載入的 Angular 模組和 AppModule 會建立自己的例項。

Providing primitive values

假設我們的任務是向 Internet Explorer 11 使用者顯示棄用通知。 我們將建立一個 InjectionToken

這允許我們將布林標誌注入服務、元件等。 同時,我們只對每個模組注入器評估一次 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 指定執行時應該注入什麼樣的值:

// 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 建構函式,從而不再需要 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 {}

不幸的是,依賴注入令牌建構函式目前不允許我們宣告工廠提供程式依賴項。 相反,我們必須使用來自@angular/core 的注入函式。

// 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 之後,如何例項化具有依賴關係的 injection token 的程式碼示例。

注入函式從提供它的模組注入器中注入依賴項——在這個例子中是根模組注入器。 它可以被 tree-shakable 提供者中的工廠使用。 Tree-shakable 基於類的服務也可以在它們的建構函式和屬性初始化器中使用它。

Providing platform-specific APIs

為了利用特定於平臺的 API 並確保高水平的可測試性,我們可以使用依賴注入令牌來提供 API。

讓我們看一個 Location 的例子。 在瀏覽器中,它可用作全域性變數 location,另外在 document.location 中。 它在 TypeScript 中具有 Location 型別。 如果你在你的一個服務中通過型別注入它,你可能沒有意識到 Location 是一個介面。

介面是 TypeScript 中的編譯時工件,Angular 無法將其用作依賴注入令牌。 Angular 在執行時解決依賴關係,因此我們必須使用在執行時可用的軟體工件。 很像 Map 或 WeakMap 的鍵。

相反,我們建立了一個依賴注入令牌並使用它來將 Location 注入到例如服務中。

// 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 的依賴項。 我們可以建立另一個依賴注入令牌,但事實證明 Angular 已經為這個特定於平臺的 API 公開了一個——由@angular/common 包匯出的 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 {}

下面是新式寫法:

和以前一樣,我們可以通過將工廠傳遞給依賴注入令牌建構函式來擺脫 Angular 模組。 請記住,我們必須將工廠依賴項轉換為對注入的呼叫。

// 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',
});

現在我們有了一種為特定於平臺的 API 建立通用訪問器的方法。 這在測試依賴它們的 declarable 和服務時將證明是有用的。

Testing tree-shakable dependencies

在測試 tree-shakable 依賴項時,重要的是要注意依賴項預設由工廠提供,作為選項傳遞給 Injectable 和 InjectionToken。

為了覆蓋可搖樹依賴,我們使用 TestBed.overrideProvider,例如 TestBed.overrideProvider(userAgentToken, { useValue: 'TestBrowser' })。

Angular 模組中的提供者僅在將 Angular 模組新增到 Angular 測試模組匯入時才用於測試,例如 TestBed.configureTestingModule({imports: [InternetExplorerModule] })。

Do tree-shakable dependencies matter?

Tree-shakable 依賴對於小型應用程式沒有多大意義,我們應該能夠很容易地判斷一個服務是否在實際使用中。

相反,假設我們建立了一個供多個應用程式使用的共享服務庫。 應用程式包現在可以忽略在該特定應用程式中未使用的服務。 這對於具有共享庫的 monorepo 工作區和 multirepo 專案都很有用。

Tree-shakable 依賴項對於 Angular 庫也很重要。 例如,假設我們在應用程式中匯入了所有 Angular Material 模組,但僅使用了部分元件及其相關的基於類的服務。 因為 Angular Material 提供了搖樹服務,所以我們的應用程式包中只包含我們使用的服務。

Summary

我們已經研究了使用 tree-shakable 提供程式配置注入器的現代選項。 與前 Angular 6 時代的提供者相比,可搖動樹的依賴項通常更容易推理且不易出錯。

來自共享庫和 Angular 庫的未使用的 tree-shakable 服務在編譯時被刪除,從而產生更小的包。