Angular9 DI 實(shí)戰(zhàn)

2020-07-03 17:05 更新

嵌套的服務(wù)依賴

這些被注入服務(wù)的消費(fèi)者不需要知道如何創(chuàng)建這個(gè)服務(wù)。新建和緩存這個(gè)服務(wù)是依賴注入器的工作。消費(fèi)者只要讓依賴注入框架知道它需要哪些依賴項(xiàng)就可以了。

有時(shí)候一個(gè)服務(wù)依賴其它服務(wù)...而其它服務(wù)可能依賴另外的更多服務(wù)。 依賴注入框架會(huì)負(fù)責(zé)正確的順序解析這些嵌套的依賴項(xiàng)。 在每一步,依賴的使用者只要在它的構(gòu)造函數(shù)里簡單聲明它需要什么,框架就會(huì)完成所有剩下的事情。

下面的例子往 AppComponent 里聲明它依賴 LoggerServiceUserContext。

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

constructor(logger: LoggerService, public userContext: UserContextService) {
  userContext.loadUser(this.userId);
  logger.logInfo('AppComponent initialized');
}

UserContext 轉(zhuǎn)而依賴 LoggerServiceUserService(這個(gè)服務(wù)用來收集特定用戶信息)。

Path:"user-context.service.ts (injection)" 。

@Injectable({
  providedIn: 'root'
})
export class UserContextService {
  constructor(private userService: UserService, private loggerService: LoggerService) {
  }
}

當(dāng) Angular 新建 AppComponent 時(shí),依賴注入框架會(huì)先創(chuàng)建一個(gè) LoggerService 的實(shí)例,然后創(chuàng)建 UserContextService 實(shí)例。 UserContextService 也需要框架剛剛創(chuàng)建的這個(gè) LoggerService 實(shí)例,這樣框架才能為它提供同一個(gè)實(shí)例。UserContextService 還需要框架創(chuàng)建過的 UserService。 UserService 沒有其它依賴,所以依賴注入框架可以直接 new 出該類的一個(gè)實(shí)例,并把它提供給 UserContextService 的構(gòu)造函數(shù)。

父組件 AppComponent 不需要了解這些依賴的依賴。 只要在構(gòu)造函數(shù)中聲明自己需要的依賴即可(這里是 LoggerServiceUserContextService),框架會(huì)幫你解析這些嵌套的依賴。

當(dāng)所有的依賴都就位之后,AppComponent 就會(huì)顯示該用戶的信息。

把服務(wù)的范圍限制到某個(gè)組件的子樹下

Angular 應(yīng)用程序有多個(gè)依賴注入器,組織成一個(gè)與組件樹平行的樹狀結(jié)構(gòu)。 每個(gè)注入器都會(huì)創(chuàng)建依賴的一個(gè)單例。在所有該注入器負(fù)責(zé)提供服務(wù)的地方,所提供的都是同一個(gè)實(shí)例。 可以在注入器樹的任何層級(jí)提供和建立特定的服務(wù)。這意味著,如果在多個(gè)注入器中提供該服務(wù),那么該服務(wù)也就會(huì)有多個(gè)實(shí)例。

由根注入器提供的依賴可以注入到應(yīng)用中任何地方的任何組件中。 但有時(shí)候你可能希望把服務(wù)的有效性限制到應(yīng)用程序的一個(gè)特定區(qū)域。 比如,你可能希望用戶明確選擇一個(gè)服務(wù),而不是讓根注入器自動(dòng)提供它。

通過在組件樹的子級(jí)根組件中提供服務(wù),可以把一個(gè)被注入服務(wù)的作用域局限在應(yīng)用程序結(jié)構(gòu)中的某個(gè)分支中。 這個(gè)例子中展示了如何通過把服務(wù)添加到子組件 @Component() 裝飾器的 providers 數(shù)組中,來為 HeroesBaseComponent 提供另一個(gè) HeroService 實(shí)例:

Path:"src/app/sorted-heroes.component.ts (HeroesBaseComponent excerpt)" 。

@Component({
  selector: 'app-unsorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
  constructor(private heroService: HeroService) { }
}

當(dāng) Angular 新建 HeroBaseComponent 的時(shí)候,它會(huì)同時(shí)新建一個(gè) HeroService 實(shí)例,該實(shí)例只在該組件及其子組件(如果有)中可見。

也可以在應(yīng)用程序別處的另一個(gè)組件里提供 HeroService。這樣就會(huì)導(dǎo)致在另一個(gè)注入器中存在該服務(wù)的另一個(gè)實(shí)例。

這個(gè)例子中,局部化的 HeroService 單例,遍布整份范例代碼,包括 HeroBiosComponent、HeroOfTheMonthComponentHeroBaseComponent。 這些組件每個(gè)都有自己的 HeroService 實(shí)例,用來管理獨(dú)立的英雄庫。

多個(gè)服務(wù)實(shí)例(沙箱式隔離)

在組件樹的同一個(gè)級(jí)別上,有時(shí)需要一個(gè)服務(wù)的多個(gè)實(shí)例。

一個(gè)用來保存其伴生組件的實(shí)例狀態(tài)的服務(wù)就是個(gè)好例子。 每個(gè)組件都需要該服務(wù)的單獨(dú)實(shí)例。 每個(gè)服務(wù)有自己的工作狀態(tài),與其它組件的服務(wù)和狀態(tài)隔離。這叫做沙箱化,因?yàn)槊總€(gè)服務(wù)和組件實(shí)例都在自己的沙箱里運(yùn)行。

在這個(gè)例子中,HeroBiosComponent 渲染了 HeroBioComponent 的三個(gè)實(shí)例。

Path:"ap/hero-bios.component.ts" 。

@Component({
  selector: 'app-hero-bios',
  template: `
    <app-hero-bio [heroId]="1"></app-hero-bio>
    <app-hero-bio [heroId]="2"></app-hero-bio>
    <app-hero-bio [heroId]="3"></app-hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosComponent {
}

每個(gè) HeroBioComponent 都能編輯一個(gè)英雄的生平。HeroBioComponent 依賴 HeroCacheService 服務(wù)來對該英雄進(jìn)行讀取、緩存和執(zhí)行其它持久化操作。

Path:"src/app/hero-cache.service.ts" 。

@Injectable()
export class HeroCacheService {
  hero: Hero;
  constructor(private heroService: HeroService) {}


  fetchCachedHero(id: number) {
    if (!this.hero) {
      this.hero = this.heroService.getHeroById(id);
    }
    return this.hero;
  }
}

這三個(gè) HeroBioComponent 實(shí)例不能共享同一個(gè) HeroCacheService 實(shí)例。否則它們會(huì)相互沖突,爭相把自己的英雄放在緩存里面。

它們應(yīng)該通過在自己的元數(shù)據(jù)(metadata)providers 數(shù)組里面列出 HeroCacheService, 這樣每個(gè) HeroBioComponent 就能擁有自己獨(dú)立的 HeroCacheService 實(shí)例了。

Path:"src/app/hero-bio.component.ts" 。

@Component({
  selector: 'app-hero-bio',
  template: `
    <h4>{{hero.name}}</h4>
    <ng-content></ng-content>
    <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
  providers: [HeroCacheService]
})


export class HeroBioComponent implements OnInit  {
  @Input() heroId: number;


  constructor(private heroCache: HeroCacheService) { }


  ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }


  get hero() { return this.heroCache.hero; }
}

父組件 HeroBiosComponent 把一個(gè)值綁定到 heroIdngOnInit 把該 id 傳遞到服務(wù),然后服務(wù)獲取和緩存英雄。hero 屬性的 getter 從服務(wù)里面獲取緩存的英雄,并在模板里顯示它綁定到屬性值。

確認(rèn)三個(gè) HeroBioComponent 實(shí)例擁有自己獨(dú)立的英雄數(shù)據(jù)緩存。

使用參數(shù)裝飾器來限定依賴查找方式

當(dāng)類需要某個(gè)依賴項(xiàng)時(shí),該依賴項(xiàng)就會(huì)作為參數(shù)添加到類的構(gòu)造函數(shù)中。 當(dāng) Angular 需要實(shí)例化該類時(shí),就會(huì)調(diào)用 DI 框架來提供該依賴。 默認(rèn)情況下,DI 框架會(huì)在注入器樹中查找一個(gè)提供者,從該組件的局部注入器開始,如果需要,則沿著注入器樹向上冒泡,直到根注入器。

  • 第一個(gè)配置過該提供者的注入器就會(huì)把依賴(服務(wù)實(shí)例或值)提供給這個(gè)構(gòu)造函數(shù)。

  • 如果在根注入器中也沒有找到提供者,則 DI 框架將會(huì)拋出一個(gè)錯(cuò)誤。

通過在類的構(gòu)造函數(shù)中對服務(wù)參數(shù)使用參數(shù)裝飾器,可以提供一些選項(xiàng)來修改默認(rèn)的搜索行為。

用 @Optional 來讓依賴是可選的,以及使用 @Host 來限定搜索方式

依賴可以注冊在組件樹的任何層級(jí)上。 當(dāng)組件請求某個(gè)依賴時(shí),Angular 會(huì)從該組件的注入器找起,沿著注入器樹向上,直到找到了第一個(gè)滿足要求的提供者。如果沒找到依賴,Angular 就會(huì)拋出一個(gè)錯(cuò)誤。

某些情況下,你需要限制搜索,或容忍依賴項(xiàng)的缺失。 你可以使用組件構(gòu)造函數(shù)參數(shù)上的 @Host@Optional 這兩個(gè)限定裝飾器來修改 Angular 的搜索行為。

  • @Optional 屬性裝飾器告訴 Angular 當(dāng)找不到依賴時(shí)就返回 null

  • @Host 屬性裝飾器會(huì)禁止在宿主組件以上的搜索。宿主組件通常就是請求該依賴的那個(gè)組件。 不過,當(dāng)該組件投影進(jìn)某個(gè)父組件時(shí),那個(gè)父組件就會(huì)變成宿主。下面的例子中介紹了第二種情況。

如下例所示,這些裝飾器可以獨(dú)立使用,也可以同時(shí)使用。這個(gè) HeroBiosAndContactsComponent 是你以前見過的那個(gè) HeroBiosComponent 的修改版。

Path:"src/app/hero-bios.component.ts (HeroBiosAndContactsComponent)" 。

@Component({
  selector: 'app-hero-bios-and-contacts',
  template: `
    <app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
    <app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
    <app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosAndContactsComponent {
  constructor(logger: LoggerService) {
    logger.logInfo('Creating HeroBiosAndContactsComponent');
  }
}

注意看模板:

Path:"dependency-injection-in-action/src/app/hero-bios.component.ts" 。

template: `
  <app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  <app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
  <app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,

<hero-bio> 標(biāo)簽中是一個(gè)新的 <hero-contact> 元素。Angular 就會(huì)把相應(yīng)的 HeroContactComponent投影(transclude)進(jìn) HeroBioComponent 的視圖里, 將它放在 HeroBioComponent 模板的 <ng-content> 標(biāo)簽槽里。

Path:"src/app/hero-bio.component.ts (template)" 。

template: `
  <h4>{{hero.name}}</h4>
  <ng-content></ng-content>
  <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,

HeroContactComponent 獲得的英雄電話號(hào)碼,被投影到上面的英雄描述里,結(jié)果如下:

這里的 HeroContactComponent 演示了限定型裝飾器。

Path:"src/app/hero-contact.component.ts" 。

@Component({
  selector: 'app-hero-contact',
  template: `
  <div>Phone #: {{phoneNumber}}
  <span *ngIf="hasLogger">!!!</span></div>`
})
export class HeroContactComponent {


  hasLogger = false;


  constructor(
      @Host() // limit to the host component's instance of the HeroCacheService
      private heroCache: HeroCacheService,


      @Host()     // limit search for logger; hides the application-wide logger
      @Optional() // ok if the logger doesn't exist
      private loggerService?: LoggerService
  ) {
    if (loggerService) {
      this.hasLogger = true;
      loggerService.logInfo('HeroContactComponent can log!');
    }
  }


  get phoneNumber() { return this.heroCache.hero.phone; }


}

注意構(gòu)造函數(shù)的參數(shù)。

Path:"src/app/hero-contact.component.ts" 。

@Host() // limit to the host component's instance of the HeroCacheService
private heroCache: HeroCacheService,


@Host()     // limit search for logger; hides the application-wide logger
@Optional() // ok if the logger doesn't exist
private loggerService?: LoggerService

@Host() 函數(shù)是構(gòu)造函數(shù)屬性 heroCache 的裝飾器,確保從其父組件 HeroBioComponent 得到一個(gè)緩存服務(wù)。如果該父組件中沒有該服務(wù),Angular 就會(huì)拋出錯(cuò)誤,即使組件樹里的再上級(jí)有某個(gè)組件擁有這個(gè)服務(wù),還是會(huì)拋出錯(cuò)誤。

另一個(gè) @Host() 函數(shù)是構(gòu)造函數(shù)屬性 loggerService 的裝飾器。 在本應(yīng)用程序中只有一個(gè)在 AppComponent 級(jí)提供的 LoggerService 實(shí)例。 該宿主 HeroBioComponent 沒有自己的 LoggerService 提供者。

如果沒有同時(shí)使用 @Optional() 裝飾器的話,Angular 就會(huì)拋出錯(cuò)誤。當(dāng)該屬性帶有 @Optional() 標(biāo)記時(shí),Angular 就會(huì)把 loggerService 設(shè)置為 null,并繼續(xù)執(zhí)行組件而不會(huì)拋出錯(cuò)誤。

下面是 HeroBiosAndContactsComponent 的執(zhí)行結(jié)果:

如果注釋掉 @Host() 裝飾器,Angular 就會(huì)沿著注入器樹往上走,直到在 AppComponent 中找到該日志服務(wù)。日志服務(wù)的邏輯加了進(jìn)來,所顯示的英雄信息增加了 "!!!" 標(biāo)記,這表明確實(shí)找到了日志服務(wù)。

如果你恢復(fù)了 @Host() 裝飾器,并且注釋掉 @Optional 裝飾器,應(yīng)用就會(huì)拋出一個(gè)錯(cuò)誤,因?yàn)樗谒拗鹘M件這一層找不到所需的 Logger。EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

使用 @Inject 指定自定義提供者

自定義提供者讓你可以為隱式依賴提供一個(gè)具體的實(shí)現(xiàn),比如內(nèi)置瀏覽器 API。下面的例子使用 InjectionToken 來提供 localStorage,將其作為 BrowserStorageService 的依賴項(xiàng)。

Path:"src/app/storage.service.ts" 。

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


export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  providedIn: 'root',
  factory: () => localStorage
});


@Injectable({
  providedIn: 'root'
})
export class BrowserStorageService {
  constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}


  get(key: string) {
    this.storage.getItem(key);
  }


  set(key: string, value: string) {
    this.storage.setItem(key, value);
  }


  remove(key: string) {
    this.storage.removeItem(key);
  }


  clear() {
    this.storage.clear();
  }
}

factory 函數(shù)返回 window 對象上的 localStorage 屬性。Inject 裝飾器修飾一個(gè)構(gòu)造函數(shù)參數(shù),用于為某個(gè)依賴提供自定義提供者?,F(xiàn)在,就可以在測試期間使用 localStorage 的 Mock API 來覆蓋這個(gè)提供者了,而不必與真實(shí)的瀏覽器 API 進(jìn)行交互。

使用 @Self 和 @SkipSelf 來修改提供者的搜索方式

注入器也可以通過構(gòu)造函數(shù)的參數(shù)裝飾器來指定范圍。下面的例子就在 Component 類的 providers 中使用瀏覽器的 sessionStorage API 覆蓋了 BROWSER_STORAGE 令牌。同一個(gè) BrowserStorageService 在構(gòu)造函數(shù)中使用 @Self@SkipSelf 裝飾器注入了兩次,來分別指定由哪個(gè)注入器來提供依賴。

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

import { Component, OnInit, Self, SkipSelf } from '@angular/core';
import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';


@Component({
  selector: 'app-storage',
  template: `
    Open the inspector to see the local/session storage keys:


    <h3>Session Storage</h3>
    <button (click)="setSession()">Set Session Storage</button>


    <h3>Local Storage</h3>
    <button (click)="setLocal()">Set Local Storage</button>
  `,
  providers: [
    BrowserStorageService,
    { provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
  ]
})
export class StorageComponent implements OnInit {


  constructor(
    @Self() private sessionStorageService: BrowserStorageService,
    @SkipSelf() private localStorageService: BrowserStorageService,
  ) { }


  ngOnInit() {
  }


  setSession() {
    this.sessionStorageService.set('hero', 'Dr Nice - Session');
  }


  setLocal() {
    this.localStorageService.set('hero', 'Dr Nice - Local');
  }
}

使用 @Self 裝飾器時(shí),注入器只在該組件的注入器中查找提供者。@SkipSelf 裝飾器可以讓你跳過局部注入器,并在注入器樹中向上查找,以發(fā)現(xiàn)哪個(gè)提供者滿足該依賴。 sessionStorageService 實(shí)例使用瀏覽器的 sessionStorage 來跟 BrowserStorageService 打交道,而 localStorageService 跳過了局部注入器,使用根注入器提供的 BrowserStorageService,它使用瀏覽器的 localStorage API。

注入組件的 DOM 元素

即便開發(fā)者極力避免,仍然會(huì)有很多視覺效果和第三方工具 (比如 jQuery) 需要訪問 DOM。這會(huì)讓你不得不訪問組件所在的 DOM 元素。

為了說明這一點(diǎn),請看屬性型指令中那個(gè) HighlightDirective 的簡化版。

Path:"src/app/highlight.directive.ts" 。

import { Directive, ElementRef, HostListener, Input } from '@angular/core';


@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {


  @Input('appHighlight') highlightColor: string;


  private el: HTMLElement;


  constructor(el: ElementRef) {
    this.el = el.nativeElement;
  }


  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'cyan');
  }


  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }


  private highlight(color: string) {
    this.el.style.backgroundColor = color;
  }
}

當(dāng)用戶把鼠標(biāo)移到 DOM 元素上時(shí),指令將指令所在的元素的背景設(shè)置為一個(gè)高亮顏色。

Angular 把構(gòu)造函數(shù)參數(shù) el 設(shè)置為注入的 ElementRef,該 ElementRef 代表了宿主的 DOM 元素,它的 nativeElement 屬性把該 DOM 元素暴露給了指令。

下面的代碼把指令的 myHighlight 屬性(Attribute)填加到兩個(gè) <div> 標(biāo)簽里,一個(gè)沒有賦值,一個(gè)賦值了顏色。

Path:"src/app/app.component.html (highlight)" 。

<div id="highlight"  class="di-component"  appHighlight>
  <h3>Hero Bios and Contacts</h3>
  <div appHighlight="yellow">
    <app-hero-bios-and-contacts></app-hero-bios-and-contacts>
  </div>
</div>

下圖顯示了鼠標(biāo)移到 <hero-bios-and-contacts> 標(biāo)簽上的效果:

使用提供者來定義依賴

為了從依賴注入器中獲取服務(wù),你必須傳給它一個(gè)令牌。 Angular 通常會(huì)通過指定構(gòu)造函數(shù)參數(shù)以及參數(shù)的類型來處理它。 參數(shù)的類型可以用作注入器的查閱令牌。 Angular 會(huì)把該令牌傳給注入器,并把它的結(jié)果賦給相應(yīng)的參數(shù)。

下面是一個(gè)典型的例子。

Path:"src/app/hero-bios.component.ts (component constructor injection)" 。

constructor(logger: LoggerService) {
  logger.logInfo('Creating HeroBiosComponent');
}

Angular 會(huì)要求注入器提供與 LoggerService 相關(guān)的服務(wù),并把返回的值賦給 logger 參數(shù)。

如果注入器已經(jīng)緩存了與該令牌相關(guān)的服務(wù)實(shí)例,那么它就會(huì)直接提供此實(shí)例。 如果它沒有,它就要使用與該令牌相關(guān)的提供者來創(chuàng)建一個(gè)。

如果注入器無法根據(jù)令牌在自己內(nèi)部找到對應(yīng)的提供者,它便將請求移交給它的父級(jí)注入器,這個(gè)過程不斷重復(fù),直到?jīng)]有更多注入器為止。 如果沒找到,注入器就拋出一個(gè)錯(cuò)誤...除非這個(gè)請求是可選的。

新的注入器沒有提供者。 Angular 會(huì)使用一組首選提供者來初始化它本身的注入器。 你必須為自己應(yīng)用程序特有的依賴項(xiàng)來配置提供者。

定義提供者

用于實(shí)例化類的默認(rèn)方法不一定總適合用來創(chuàng)建依賴。你可以到依賴提供者部分查看其它方法。 HeroOfTheMonthComponent 例子示范了一些替代方案,展示了為什么需要它們。 它看起來很簡單:一些屬性和一些由 logger 生成的日志。

它背后的代碼定制了 DI 框架提供依賴項(xiàng)的方法和位置。 這個(gè)例子闡明了通過提供對象字面量來把對象的定義和 DI 令牌關(guān)聯(lián)起來的另一種方式。

Path:"hero-of-the-month.component.ts" 。

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


import { DateLoggerService } from './date-logger.service';
import { Hero }              from './hero';
import { HeroService }       from './hero.service';
import { LoggerService }     from './logger.service';
import { MinimalLogger }     from './minimal-logger.service';
import { RUNNERS_UP,
         runnersUpFactory }  from './runners-up';


@Component({
  selector: 'app-hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  providers: [
    { provide: Hero,          useValue:    someHero },
    { provide: TITLE,         useValue:   'Hero of the Month' },
    { provide: HeroService,   useClass:    HeroService },
    { provide: LoggerService, useClass:    DateLoggerService },
    { provide: MinimalLogger, useExisting: LoggerService },
    { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
  ]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];


  constructor(
      logger: MinimalLogger,
      public heroOfTheMonth: Hero,
      @Inject(RUNNERS_UP) public runnersUp: string,
      @Inject(TITLE) public title: string)
  {
    this.logs = logger.logs;
    logger.logInfo('starting up');
  }
}

providers 數(shù)組展示了你可以如何使用其它的鍵來定義提供者:useValueuseClass、useExistinguseFactory

值提供者:useValue

useValue 鍵讓你可以為 DI 令牌關(guān)聯(lián)一個(gè)固定的值。 使用該技巧來進(jìn)行運(yùn)行期常量設(shè)置,比如網(wǎng)站的基礎(chǔ)地址和功能標(biāo)志等。 你也可以在單元測試中使用值提供者,來用一個(gè) Mock 數(shù)據(jù)來代替一個(gè)生產(chǎn)環(huán)境下的數(shù)據(jù)服務(wù)。

HeroOfTheMonthComponent 例子中有兩個(gè)值-提供者。

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: Hero,          useValue:    someHero },
{ provide: TITLE,         useValue:   'Hero of the Month' },

  • 第一處提供了用于 Hero 令牌的 Hero 類的現(xiàn)有實(shí)例,而不是要求注入器使用 new 來創(chuàng)建一個(gè)新實(shí)例或使用它自己的緩存實(shí)例。這里令牌就是這個(gè)類本身。

  • 第二處為 TITLE 令牌指定了一個(gè)字符串字面量資源。 TITLE 提供者的令牌不是一個(gè)類,而是一個(gè)特別的提供者查詢鍵,名叫InjectionToken,表示一個(gè) InjectionToken 實(shí)例。

你可以把 InjectionToken 用作任何類型的提供者的令牌,但是當(dāng)依賴是簡單類型(比如字符串、數(shù)字、函數(shù))時(shí),它會(huì)特別有用。

一個(gè)值-提供者的值必須在指定之前定義。 比如標(biāo)題字符串就是立即可用的。 該例中的 someHero 變量是以前在如下的文件中定義的。 你不能使用那些要等以后才能定義其值的變量。

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

const someHero = new Hero(42, 'Magma', 'Had a great month!', '555-555-5555');

其它類型的提供者都會(huì)惰性創(chuàng)建它們的值,也就是說只在需要注入它們的時(shí)候才創(chuàng)建。

類提供者:useClass

useClass 提供的鍵讓你可以創(chuàng)建并返回指定類的新實(shí)例。

你可以使用這類提供者來為公共類或默認(rèn)類換上一個(gè)替代實(shí)現(xiàn)。比如,這個(gè)替代實(shí)現(xiàn)可以實(shí)現(xiàn)一種不同的策略來擴(kuò)展默認(rèn)類,或在測試環(huán)境中模擬真實(shí)類的行為。

請看下面 HeroOfTheMonthComponent 里的兩個(gè)例子:

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: HeroService,   useClass:    HeroService },
{ provide: LoggerService, useClass:    DateLoggerService },

第一個(gè)提供者是展開了語法糖的,是一個(gè)典型情況的展開。一般來說,被新建的類(HeroService)同時(shí)也是該提供者的注入令牌。 通常都選用縮寫形式,完整形式可以讓細(xì)節(jié)更明確。

第二個(gè)提供者使用 DateLoggerService 來滿足 LoggerService。該 LoggerServiceAppComponent 級(jí)別已經(jīng)被注冊。當(dāng)這個(gè)組件要求 LoggerService 的時(shí)候,它得到的卻是 DateLoggerService 服務(wù)的實(shí)例。

這個(gè)組件及其子組件會(huì)得到 DateLoggerService 實(shí)例。這個(gè)組件樹之外的組件得到的仍是 LoggerService 實(shí)例。

DateLoggerServiceLoggerService 繼承;它把當(dāng)前的日期/時(shí)間附加到每條信息上。

Path:"src/app/date-logger.service.ts" 。

@Injectable({
  providedIn: 'root'
})
export class DateLoggerService extends LoggerService
{
  logInfo(msg: any)  { super.logInfo(stamp(msg)); }
  logDebug(msg: any) { super.logInfo(stamp(msg)); }
  logError(msg: any) { super.logError(stamp(msg)); }
}


function stamp(msg: any) { return msg + ' at ' + new Date(); }

別名提供者:useExisting

useExisting 提供了一個(gè)鍵,讓你可以把一個(gè)令牌映射成另一個(gè)令牌。實(shí)際上,第一個(gè)令牌就是第二個(gè)令牌所關(guān)聯(lián)的服務(wù)的別名,這樣就創(chuàng)建了訪問同一個(gè)服務(wù)對象的兩種途徑。

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: MinimalLogger, useExisting: LoggerService },

你可以使用別名接口來窄化 API。下面的例子中使用別名就是為了這個(gè)目的。

想象 LoggerService 有個(gè)很大的 API 接口,遠(yuǎn)超過現(xiàn)有的三個(gè)方法和一個(gè)屬性。你可能希望把 API 接口收窄到只有兩個(gè)你確實(shí)需要的成員。在這個(gè)例子中,MinimalLogger類-接口,就這個(gè) API 成功縮小到了只有兩個(gè)成員:

Path:"src/app/minimal-logger.service.ts" 。

// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}

下面的例子在一個(gè)簡化版的 HeroOfTheMonthComponent 中使用 MinimalLogger。

Path:"src/app/hero-of-the-month.component.ts (minimal version)" 。

@Component({
  selector: 'app-hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  // TODO: move this aliasing, `useExisting` provider to the AppModule
  providers: [{ provide: MinimalLogger, useExisting: LoggerService }]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];
  constructor(logger: MinimalLogger) {
    logger.logInfo('starting up');
  }
}

HeroOfTheMonthComponent 構(gòu)造函數(shù)的 logger 參數(shù)是一個(gè) MinimalLogger 類型,在支持 TypeScript 感知的編輯器里,只能看到它的兩個(gè)成員 logslogInfo

實(shí)際上,Angularlogger 參數(shù)設(shè)置為注入器里 LoggerService 令牌下注冊的完整服務(wù),該令牌恰好是以前提供的那個(gè) DateLoggerService 實(shí)例。

在下面的圖片中,顯示了日志日期,可以確認(rèn)這一點(diǎn):

工廠提供者:useFactory

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

注入器通過調(diào)用你用 useFactory 鍵指定的工廠函數(shù)來提供該依賴的值。 注意,提供者的這種形態(tài)還有第三個(gè)鍵 deps,它指定了供 useFactory 函數(shù)使用的那些依賴。

使用這項(xiàng)技術(shù),可以用包含了一些依賴服務(wù)和本地狀態(tài)輸入的工廠函數(shù)來建立一個(gè)依賴對象。

這個(gè)依賴對象(由工廠函數(shù)返回的)通常是一個(gè)類實(shí)例,不過也可以是任何其它東西。 在這個(gè)例子中,依賴對象是一個(gè)表示 "月度英雄" 參賽者名稱的字符串。

在這個(gè)例子中,局部狀態(tài)是數(shù)字 2,也就是組件應(yīng)該顯示的參賽者數(shù)量。 該狀態(tài)的值傳給了 runnersUpFactory() 作為參數(shù)。 runnersUpFactory() 返回了提供者的工廠函數(shù),它可以使用傳入的狀態(tài)值和注入的服務(wù) HeroHeroService。

Path:"runners-up.ts (excerpt)" 。

export function runnersUpFactory(take: number) {
  return (winner: Hero, heroService: HeroService): string => {
    /* ... */
  };
};

runnersUpFactory() 返回的提供者的工廠函數(shù)返回了實(shí)際的依賴對象,也就是表示名字的字符串。

  • 這個(gè)返回的函數(shù)需要一個(gè) Hero 和一個(gè) HeroService 參數(shù)。

Angular 根據(jù) deps 數(shù)組中指定的兩個(gè)令牌來提供這些注入?yún)?shù)。

  • 該函數(shù)返回名字的字符串,Angular 可以把它們注入到 HeroOfTheMonthComponentrunnersUp 參數(shù)中。

該函數(shù)從 HeroService 中接受候選的英雄,從中取 2 個(gè)參加競賽,并把他們的名字串接起來返回。

提供替代令牌:類接口與 'InjectionToken'

當(dāng)使用類作為令牌,同時(shí)也把它作為返回依賴對象或服務(wù)的類型時(shí),Angular 依賴注入使用起來最容易。

但令牌不一定都是類,就算它是一個(gè)類,它也不一定都返回類型相同的對象。這是下一節(jié)的主題。

類-接口

前面的月度英雄的例子使用了 MinimalLogger 類作為 LoggerService 提供者的令牌。

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: MinimalLogger, useExisting: LoggerService },

MinimalLogger 是一個(gè)抽象類。

Path:"dependency-injection-in-action/src/app/minimal-logger.service.ts" 。



// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}

你通常從一個(gè)可擴(kuò)展的抽象類繼承。但這個(gè)應(yīng)用中并沒有類會(huì)繼承 MinimalLogger。

LoggerServiceDateLoggerService本可以從 MinimalLogger 中繼承。 它們也可以實(shí)現(xiàn) MinimalLogger,而不用單獨(dú)定義接口。 但它們沒有。 MinimalLogger 在這里僅僅被用作一個(gè) "依賴注入令牌"。

當(dāng)你通過這種方式使用類時(shí),它稱作類接口。

就像 DI 提供者中提到的那樣,接口不是有效的 DI 令牌,因?yàn)樗?TypeScript 自己用的,在運(yùn)行期間不存在。使用這種抽象類接口不但可以獲得像接口一樣的強(qiáng)類型,而且可以像普通類一樣把它用作提供者令牌。

類接口應(yīng)該只定義允許它的消費(fèi)者調(diào)用的成員。窄的接口有助于解耦該類的具體實(shí)現(xiàn)和它的消費(fèi)者。

用類作為接口可以讓你獲得真實(shí) JavaScript 對象中的接口的特性。 但是,為了最小化內(nèi)存開銷,該類應(yīng)該是沒有實(shí)現(xiàn)的。 對于構(gòu)造函數(shù),MinimalLogger 會(huì)轉(zhuǎn)譯成未優(yōu)化過的、預(yù)先最小化過的 JavaScript。

Path:"dependency-injection-in-action/src/app/minimal-logger.service.ts" 。

var MinimalLogger = (function () {
  function MinimalLogger() {}
  return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);

注:

只要不實(shí)現(xiàn)它,不管添加多少成員,它都不會(huì)增長大小,因?yàn)檫@些成員雖然是有類型的,但卻沒有實(shí)現(xiàn)。

你可以再看看 TypeScript 的 MinimalLogger 類,確定一下它是沒有實(shí)現(xiàn)的。

'InjectionToken' 對象

依賴對象可以是一個(gè)簡單的值,比如日期,數(shù)字和字符串,或者一個(gè)無形的對象,比如數(shù)組和函數(shù)。

這樣的對象沒有應(yīng)用程序接口,所以不能用一個(gè)類來表示。更適合表示它們的是:唯一的和符號(hào)性的令牌,一個(gè) JavaScript 對象,擁有一個(gè)友好的名字,但不會(huì)與其它的同名令牌發(fā)生沖突。

InjectionToken 具有這些特征。在Hero of the Month例子中遇見它們兩次,一個(gè)是 title 的值,一個(gè)是 runnersUp 工廠提供者。

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

{ provide: TITLE,         useValue:   'Hero of the Month' },
{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

這樣創(chuàng)建 TITLE 令牌:

Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。

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


export const TITLE = new InjectionToken<string>('title');

類型參數(shù),雖然是可選的,但可以向開發(fā)者和開發(fā)工具傳達(dá)類型信息。 而且這個(gè)令牌的描述信息也可以為開發(fā)者提供幫助。

注入到派生類

當(dāng)編寫一個(gè)繼承自另一個(gè)組件的組件時(shí),要格外小心。如果基礎(chǔ)組件有依賴注入,必須要在派生類中重新提供和重新注入它們,并將它們通過構(gòu)造函數(shù)傳給基類。

在這個(gè)刻意生成的例子里,SortedHeroesComponent 繼承自 HeroesBaseComponent,顯示一個(gè)被排序的英雄列表。

HeroesBaseComponent 能自己獨(dú)立運(yùn)行。它在自己的實(shí)例里要求 HeroService,用來得到英雄,并將他們按照數(shù)據(jù)庫返回的順序顯示出來。

Path:"src/app/sorted-heroes.component.ts (HeroesBaseComponent)" 。

@Component({
  selector: 'app-unsorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
  constructor(private heroService: HeroService) { }


  heroes: Array<Hero>;


  ngOnInit() {
    this.heroes = this.heroService.getAllHeroes();
    this.afterGetHeroes();
  }


  // Post-process heroes in derived class override.
  protected afterGetHeroes() {}


}

讓構(gòu)造函數(shù)保持簡單

構(gòu)造函數(shù)應(yīng)該只用來初始化變量。 這條規(guī)則讓組件在測試環(huán)境中可以放心地構(gòu)造組件,以免在構(gòu)造它們時(shí),無意中做出一些非常戲劇化的動(dòng)作(比如與服務(wù)器進(jìn)行會(huì)話)。 這就是為什么你要在 ngOnInit 里面調(diào)用 HeroService,而不是在構(gòu)造函數(shù)中。

用戶希望看到英雄按字母順序排序。與其修改原始的組件,不如派生它,新建 SortedHeroesComponent,以便展示英雄之前進(jìn)行排序。 SortedHeroesComponent 讓基類來獲取英雄。

可惜,Angular 不能直接在基類里直接注入 HeroService。必須在這個(gè)組件里再次提供 HeroService,然后通過構(gòu)造函數(shù)傳給基類。

Path:"src/app/sorted-heroes.component.ts (SortedHeroesComponent)" 。

@Component({
  selector: 'app-sorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class SortedHeroesComponent extends HeroesBaseComponent {
  constructor(heroService: HeroService) {
    super(heroService);
  }


  protected afterGetHeroes() {
    this.heroes = this.heroes.sort((h1, h2) => {
      return h1.name < h2.name ? -1 :
            (h1.name > h2.name ? 1 : 0);
    });
  }
}

現(xiàn)在,請注意 afterGetHeroes() 方法。 你的第一反應(yīng)是在 SortedHeroesComponent 組件里面建一個(gè) ngOnInit 方法來做排序。但是 Angular 會(huì)先調(diào)用派生類的 ngOnInit,后調(diào)用基類的 ngOnInit, 所以可能在英雄到達(dá)之前就開始排序。這就產(chǎn)生了一個(gè)討厭的錯(cuò)誤。

覆蓋基類的 afterGetHeroes() 方法可以解決這個(gè)問題。

分析上面的這些復(fù)雜性是為了強(qiáng)調(diào)避免使用組件繼承這一點(diǎn)。

使用一個(gè)前向引用(forwardRef)來打破循環(huán)

在 TypeScript 里面,類聲明的順序是很重要的。如果一個(gè)類尚未定義,就不能引用它。

這通常不是一個(gè)問題,特別是當(dāng)你遵循一個(gè)文件一個(gè)類規(guī)則的時(shí)候。 但是有時(shí)候循環(huán)引用可能不能避免。當(dāng)一個(gè)類A 引用類 B,同時(shí)'B'引用'A'的時(shí)候,你就陷入困境了:它們中間的某一個(gè)必須要先定義。

Angular 的 forwardRef() 函數(shù)建立一個(gè)間接地引用,Angular 可以隨后解析。

這個(gè)關(guān)于父查找器的例子中全都是沒辦法打破的循環(huán)類引用。

當(dāng)一個(gè)類需要引用自身的時(shí)候,你面臨同樣的困境,就像在 AlexComponentprovdiers 數(shù)組中遇到的困境一樣。 該 providers 數(shù)組是一個(gè) @Component() 裝飾器函數(shù)的一個(gè)屬性,它必須在類定義之前出現(xiàn)。

使用 forwardRef 來打破這種循環(huán):

Path:"parent-finder.component.ts (AlexComponent providers)" 。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)