Angular9 DI 提供者

2020-07-03 16:26 更新

依賴提供者會(huì)使用 DI 令牌來配置注入器,注入器會(huì)用它來提供這個(gè)依賴值的具體的、運(yùn)行時(shí)版本。 注入器依靠 "提供者配置" 來創(chuàng)建依賴的實(shí)例,并把該實(shí)例注入到組件、指令、管道和其它服務(wù)中。

你必須使用提供者來配置注入器,否則注入器就無法知道如何創(chuàng)建此依賴。 注入器創(chuàng)建服務(wù)實(shí)例的最簡單方法,就是用這個(gè)服務(wù)類本身來創(chuàng)建它。 如果你把服務(wù)類作為此服務(wù)的 DI 令牌,注入器的默認(rèn)行為就是 new 出這個(gè)類實(shí)例。

在下面這個(gè)典型的例子中,Logger 類自身提供了Logger` 的實(shí)例。

providers: [Logger]

不過,你也可以用一個(gè)替代提供者來配置注入器,這樣就可以指定另一些同樣能提供日志功能的對(duì)象。 比如:

  • 你可以提供一個(gè)替代類。

  • 你可以提供一個(gè)類似于 Logger 的對(duì)象。

  • 你的提供者可以調(diào)用一個(gè)工廠函數(shù)來創(chuàng)建 logger。

Provider 對(duì)象字面量

類提供者的語法實(shí)際上是一種簡寫形式,它會(huì)擴(kuò)展成一個(gè)由 Provider 接口定義的提供者配置對(duì)象。 下面的代碼片段展示了 providers 中給出的類會(huì)如何擴(kuò)展成完整的提供者配置對(duì)象。

providers: [Logger]

[{ provide: Logger, useClass: Logger }]

擴(kuò)展的提供者配置是一個(gè)具有兩個(gè)屬性的對(duì)象字面量。

  • provide 屬性存有令牌,它作為一個(gè) key,在定位依賴值和配置注入器時(shí)使用。

  • 第二個(gè)屬性是一個(gè)提供者定義對(duì)象,它告訴注入器要如何創(chuàng)建依賴值。 提供者定義對(duì)象中的 key 可以是 useClass —— 就像這個(gè)例子中一樣。 也可以是 useExisting、useValueuseFactory。 每一個(gè) key 都用于提供一種不同類型的依賴,我們稍后會(huì)討論。

替代類提供者

不同的類都可用于提供相同的服務(wù)。 比如,下面的代碼告訴注入器,當(dāng)組件使用 Logger 令牌請(qǐng)求日志對(duì)象時(shí),給它返回一個(gè) BetterLogger 實(shí)例。

[{ provide: Logger, useClass: BetterLogger }]

擴(kuò)展的提供者配置是一個(gè)具有兩個(gè)屬性的對(duì)象字面量。

  • provide 屬性存有令牌,它作為一個(gè) key,在定位依賴值和配置注入器時(shí)使用。

  • 第二個(gè)屬性是一個(gè)提供者定義對(duì)象,它告訴注入器要如何創(chuàng)建依賴值。 提供者定義對(duì)象中的 key 可以是 useClass —— 就像這個(gè)例子中一樣。 也可以是 useExisting、useValueuseFactory。 每一個(gè) key 都用于提供一種不同類型的依賴,我們稍后會(huì)討論。

替代類提供者

不同的類都可用于提供相同的服務(wù)。 比如,下面的代碼告訴注入器,當(dāng)組件使用 Logger 令牌請(qǐng)求日志對(duì)象時(shí),給它返回一個(gè) BetterLogger 實(shí)例。

[{ provide: Logger, useClass: BetterLogger }]

帶依賴的類提供者

另一個(gè)類 EvenBetterLogger 可能要在日志信息里顯示用戶名。 這個(gè) logger 要從注入的 UserService 實(shí)例中來獲取該用戶。

@Injectable()
export class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }


  log(message: string) {
    let name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

注入器需要提供這個(gè)新的日志服務(wù)以及該服務(wù)所依賴的 UserService 對(duì)象。 使用 useClass 作為提供者定義對(duì)象的 key,來配置一個(gè) logger 的替代品,比如 BetterLogger。 下面的數(shù)組同時(shí)在父模塊和組件的 providers 元數(shù)據(jù)選項(xiàng)中指定了這些提供者。

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

別名類提供者

假設(shè)老的組件依賴于 OldLogger 類。OldLoggerNewLogger 的接口相同,但是由于某種原因,我們沒法修改老的組件來使用 NewLogger。

當(dāng)老的組件要使用 OldLogger 記錄信息時(shí),你可能希望改用 NewLogger 的單例來處理它。 在這種情況下,無論某個(gè)組件請(qǐng)求老的 logger 還是新的 logger,依賴注入器都應(yīng)該注入這個(gè) NewLogger 的單例。 也就是說 OldLogger 應(yīng)該是 NewLogger 的別名。

如果你試圖用 useClassOldLogger 指定一個(gè)別名 NewLogger,就會(huì)在應(yīng)用中得到 NewLogger 的兩個(gè)不同的實(shí)例。

[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

要確保只有一個(gè) NewLogger 實(shí)例,就要用 useExisting 來為 OldLogger 指定別名。

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

值提供者

有時(shí)候,提供一個(gè)現(xiàn)成的對(duì)象會(huì)比要求注入器從類去創(chuàng)建更簡單一些。 如果要注入一個(gè)你已經(jīng)創(chuàng)建過的對(duì)象,請(qǐng)使用 useValue 選項(xiàng)來配置該注入器。

下面的代碼定義了一個(gè)變量,用來創(chuàng)建這樣一個(gè)能扮演 logger 角色的對(duì)象。

// An object in the shape of the logger service
function silentLoggerFn() {}


export const SilentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: silentLoggerFn
};

下面的提供者定義對(duì)象使用 useValue 作為 key 來把該變量與 Logger 令牌關(guān)聯(lián)起來。

[{ provide: Logger, useValue: SilentLogger }]

非類依賴

并非所有的依賴都是類。 有時(shí)候你會(huì)希望注入字符串、函數(shù)或?qū)ο蟆?/p>

應(yīng)用通常會(huì)用大量的小型參數(shù)來定義配置對(duì)象,比如應(yīng)用的標(biāo)題或 Web API 端點(diǎn)的地址。 這些配置對(duì)象不一定總是類的實(shí)例。 它們還可能是對(duì)象字面量,如下例所示。

Path:"src/app/app.config.ts (excerpt)" 。

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

TypeScript 接口不是有效的令牌

HERO_DI_CONFIG 常量滿足 AppConfig 接口的要求。 不幸的是,你不能用 TypeScript 的接口作為令牌。 在 TypeScript 中,接口是一個(gè)設(shè)計(jì)期的概念,無法用作 DI 框架在運(yùn)行期所需的令牌。

// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]

// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

如果你曾經(jīng)在強(qiáng)類型語言中使用過依賴注入功能,這一點(diǎn)可能看起來有點(diǎn)奇怪,那些語言都優(yōu)先使用接口作為查找依賴的 key。 不過,JavaScript 沒有接口,所以,當(dāng) TypeScript 轉(zhuǎn)譯成 JavaScript 時(shí),接口也就消失了。 在運(yùn)行期間,沒有留下任何可供 Angular 進(jìn)行查找的接口類型信息。

替代方案之一是以類似于 AppModule 的方式,在 NgModule 中提供并注入這個(gè)配置對(duì)象。

Path:"src/app/app.module.ts (providers)" 。

providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],

另一個(gè)為非類依賴選擇提供者令牌的解決方案是定義并使用 InjectionToken 對(duì)象。 下面的例子展示了如何定義那樣一個(gè)令牌。

Path:"src/app/app.config.ts" 。

import { InjectionToken } from '@angular/core';


export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

雖然類型參數(shù)在這里是可選的,不過還是能把此依賴的類型信息傳達(dá)給開發(fā)人員和開發(fā)工具。 這個(gè)令牌的描述則是開發(fā)人員的另一個(gè)助力。

使用 InjectionToken 對(duì)象注冊(cè)依賴提供者:

Path:"src/app/app.component.ts" 。

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

雖然 AppConfig 接口在依賴注入時(shí)沒有任何作用,但它可以為該組件類中的這個(gè)配置對(duì)象指定類型信息。

工廠提供者

有時(shí)候你需要?jiǎng)討B(tài)創(chuàng)建依賴值,創(chuàng)建時(shí)需要的信息你要等運(yùn)行期間才能拿到。 比如,你可能需要某個(gè)在瀏覽器會(huì)話過程中會(huì)被反復(fù)修改的信息,而且這個(gè)可注入服務(wù)還不能獨(dú)立訪問這個(gè)信息的源頭。

這種情況下,你可以使用工廠提供者。 當(dāng)需要從第三方庫創(chuàng)建依賴項(xiàng)實(shí)例時(shí),工廠提供者也很有用,因?yàn)榈谌綆觳皇菫?DI 而設(shè)計(jì)的。

比如,假設(shè) HeroService 必須對(duì)普通用戶隱藏秘密英雄,只有得到授權(quán)的用戶才能看到他們。

EvenBetterLogger 一樣,HeroService 需要知道該用戶是否有權(quán)查看秘密英雄。 而認(rèn)證信息可能會(huì)在應(yīng)用的單個(gè)會(huì)話中發(fā)生變化,比如你改用另一個(gè)用戶登錄。

假設(shè)你不希望直接把 UserService 注入到 HeroService 中,因?yàn)槟悴幌M堰@個(gè)服務(wù)與那些高度敏感的信息牽扯到一起。 這樣 HeroService 就無法直接訪問到用戶信息,來決定誰有權(quán)訪問,誰沒有。

要解決這個(gè)問題,我們給 HeroService 的構(gòu)造函數(shù)一個(gè)邏輯型標(biāo)志,以控制是否顯示秘密英雄。

Path:"src/app/heroes/hero.service.ts (excerpt)" 。

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }


getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

你可以注入 Logger 但是不能注入 isAuthorized 標(biāo)志。不過你可以改用工廠提供者來為 HeroService 創(chuàng)建一個(gè)新的 logger 實(shí)例。

工廠提供者需要一個(gè)工廠函數(shù)。

Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

雖然 HeroService 不能訪問 UserService,但是工廠函數(shù)可以。 你把 LoggerUserService 注入到了工廠提供者中,并讓注入器把它們傳給這個(gè)工廠函數(shù)。

Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

  • useFactory 字段告訴 Angular 該提供者是一個(gè)工廠函數(shù),該函數(shù)的實(shí)現(xiàn)代碼是 heroServiceFactory。

  • deps 屬性是一個(gè)提供者令牌數(shù)組。 LoggerUserService 類作為它們自己的類提供者令牌使用。 注入器解析這些令牌,并把與之對(duì)應(yīng)的服務(wù)注入到相應(yīng)的工廠函數(shù)參數(shù)表中。

注意,你把這個(gè)工廠提供者保存到了一個(gè)導(dǎo)出的變量 heroServiceProvider 中。 這個(gè)額外的步驟讓工廠提供者可被復(fù)用。 你可以在任何需要它的地方用這個(gè)變量來配置 HeroService 的提供者。 在這個(gè)例子中,你只在 HeroesComponent 中用到了它。你在該組件元數(shù)據(jù)的 providers 數(shù)組中用 heroServiceProvider 替換了 HeroService。

下面并列顯示了新舊實(shí)現(xiàn)。

  1. Path:"src/app/heroes/heroes.component (v3)" 。

    import { Component }          from '@angular/core';
    import { heroServiceProvider } from './hero.service.provider';


    @Component({
      selector: 'app-heroes',
      providers: [ heroServiceProvider ],
      template: `
        <h2>Heroes</h2>
        <app-hero-list></app-hero-list>
      `
    })
    export class HeroesComponent { }

  1. Path:"src/app/heroes/heroes.component (v2)" 。

    import { Component } from '@angular/core';


    import { HeroService } from './hero.service';


    @Component({
      selector: 'app-heroes',
      providers: [ HeroService ],
      template: `
        <h2>Heroes</h2>
        <app-hero-list></app-hero-list>
      `
    })
    export class HeroesComponent { }

預(yù)定義令牌與多提供者

Angular 提供了一些內(nèi)置的注入令牌常量,你可以用它們來自定義系統(tǒng)的多種行為。

比如,你可以使用下列內(nèi)置令牌來切入 Angular 框架的啟動(dòng)和初始化過程。 提供者對(duì)象可以把任何一個(gè)注入令牌與一個(gè)或多個(gè)用來執(zhí)行應(yīng)用初始化操作的回調(diào)函數(shù)關(guān)聯(lián)起來。

  • PLATFORM_INITIALIZER:平臺(tái)初始化之后調(diào)用的回調(diào)函數(shù)。

  • APP_BOOTSTRAP_LISTENER:每個(gè)啟動(dòng)組件啟動(dòng)完成之后調(diào)用的回調(diào)函數(shù)。這個(gè)處理器函數(shù)會(huì)收到這個(gè)啟動(dòng)組件的 ComponentRef 實(shí)例。

  • APP_INITIALIZER:應(yīng)用初始化之前調(diào)用的回調(diào)函數(shù)。注冊(cè)的所有初始化器都可以(可選地)返回一個(gè) Promise。所有返回 Promise 的初始化函數(shù)都必須在應(yīng)用啟動(dòng)之前解析完。如果任何一個(gè)初始化器失敗了,該應(yīng)用就不會(huì)繼續(xù)啟動(dòng)。

該提供者對(duì)象還有第三個(gè)選項(xiàng) multi: true,把它和 APP_INITIALIZER 一起使用可以為特定的事件注冊(cè)多個(gè)處理器。

比如,當(dāng)啟動(dòng)應(yīng)用時(shí),你可以使用同一個(gè)令牌注冊(cè)多個(gè)初始化器。

export const APP_TOKENS = [
 { provide: PLATFORM_INITIALIZER, useFactory: platformInitialized, multi: true    },
 { provide: APP_INITIALIZER, useFactory: delayBootstrapping, multi: true },
 { provide: APP_BOOTSTRAP_LISTENER, useFactory: appBootstrapped, multi: true },
];

在其它地方,多個(gè)提供者也同樣可以和單個(gè)令牌關(guān)聯(lián)起來。 比如,你可以使用內(nèi)置的 NG_VALIDATORS 令牌注冊(cè)自定義表單驗(yàn)證器,還可以在提供者定義對(duì)象中使用 multi: true 屬性來為指定的驗(yàn)證器令牌提供多個(gè)驗(yàn)證器實(shí)例。 Angular 會(huì)把你的自定義驗(yàn)證器添加到現(xiàn)有驗(yàn)證器的集合中。

路由器也同樣用多個(gè)提供者關(guān)聯(lián)到了一個(gè)令牌。 當(dāng)你在單個(gè)模塊中用 RouterModule.forRootRouterModule.forChild 提供了多組路由時(shí),ROUTES 令牌會(huì)把這些不同的路由組都合并成一個(gè)單一值。

可搖樹優(yōu)化的提供者

搖樹優(yōu)化是指一個(gè)編譯器選項(xiàng),意思是把應(yīng)用中未引用過的代碼從最終生成的包中移除。 如果提供者是可搖樹優(yōu)化的,Angular 編譯器就會(huì)從最終的輸出內(nèi)容中移除應(yīng)用代碼中從未用過的服務(wù)。 這會(huì)顯著減小你的打包體積。

理想情況下,如果應(yīng)用沒有注入服務(wù),它就不應(yīng)該包含在最終輸出中。 不過,Angular 要能在構(gòu)建期間識(shí)別出該服務(wù)是否需要。 由于還可能用 injector.get(Service) 的形式直接注入服務(wù),所以 Angular 無法準(zhǔn)確識(shí)別出代碼中可能發(fā)生此注入的全部位置,因此為保險(xiǎn)起見,只能把服務(wù)包含在注入器中。 因此,在 NgModule 或 組件級(jí)別提供的服務(wù)是無法被搖樹優(yōu)化掉的。

下面這個(gè)不可搖樹優(yōu)化的 Angular 提供者的例子為 NgModule 注入器配置了一個(gè)服務(wù)提供者。

Path:"src/app/tree-shaking/service-and-modules.ts" 。

import { Injectable, NgModule } from '@angular/core';


@Injectable()
export class Service {
  doSomething(): void {
  }
}


@NgModule({
  providers: [Service],
})
export class ServiceModule {
}

你可以把該模塊導(dǎo)入到你的應(yīng)用模塊中,以便該服務(wù)可注入到你的應(yīng)用中,例子如下。

Path:"src/app/tree-shaking/app.modules.ts" 。

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([]),
    ServiceModule,
  ],
})
export class AppModule {
}

當(dāng)運(yùn)行 ngc 時(shí),它會(huì)把 AppModule 編譯到模塊工廠中,工廠包含該模塊及其導(dǎo)入的所有模塊中聲明的所有提供者。在運(yùn)行時(shí),該工廠會(huì)變成負(fù)責(zé)實(shí)例化所有這些服務(wù)的注入器。

這里搖樹優(yōu)化不起作用,因?yàn)?Angular 無法根據(jù)是否用到了其它代碼塊(服務(wù)類),來決定是否能排除這塊代碼(模塊工廠中的服務(wù)提供者定義)。要讓服務(wù)可以被搖樹優(yōu)化,關(guān)于如何構(gòu)建該服務(wù)實(shí)例的信息(即提供者定義),就應(yīng)該是服務(wù)類本身的一部分。

創(chuàng)建可搖樹優(yōu)化的提供者

只要在服務(wù)本身的 @Injectable() 裝飾器中指定,而不是在依賴該服務(wù)的 NgModule 或組件的元數(shù)據(jù)中指定,你就可以制作一個(gè)可搖樹優(yōu)化的提供者。

下面的例子展示了與上面的 ServiceModule 例子等價(jià)的可搖樹優(yōu)化的版本。

Path:"src/app/tree-shaking/service.ts" 。

@Injectable({
  providedIn: 'root',
})
export class Service {
}

該服務(wù)還可以通過配置工廠函數(shù)來實(shí)例化,如下例所示。

Path:"src/app/tree-shaking/service.0.ts" 。

@Injectable({
  providedIn: 'root',
  useFactory: () => new Service('dependency'),
})
export class Service {
  constructor(private dep: string) {
  }
}

要想覆蓋可搖樹優(yōu)化的提供者,請(qǐng)使用其它提供者來配置指定的 NgModule 或組件的注入器,只要使用 @NgModule()@Component() 裝飾器中的 providers: [] 數(shù)組就可以了。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)