Angular9 DI 實戰(zhàn)

2020-07-03 17:05 更新

嵌套的服務(wù)依賴

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

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

下面的例子往 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(這個服務(wù)用來收集特定用戶信息)。

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

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

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

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

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

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

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

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

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

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 的時候,它會同時新建一個 HeroService 實例,該實例只在該組件及其子組件(如果有)中可見。

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

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

多個服務(wù)實例(沙箱式隔離)

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

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

在這個例子中,HeroBiosComponent 渲染了 HeroBioComponent 的三個實例。

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 {
}

每個 HeroBioComponent 都能編輯一個英雄的生平。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;
  }
}

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

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

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 把一個值綁定到 heroId。ngOnInit 把該 id 傳遞到服務(wù),然后服務(wù)獲取和緩存英雄。hero 屬性的 getter 從服務(wù)里面獲取緩存的英雄,并在模板里顯示它綁定到屬性值。

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

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

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

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

  • 如果在根注入器中也沒有找到提供者,則 DI 框架將會拋出一個錯誤。

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

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

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

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

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

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

如下例所示,這些裝飾器可以獨(dú)立使用,也可以同時使用。這個 HeroBiosAndContactsComponent 是你以前見過的那個 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)簽中是一個新的 <hero-contact> 元素。Angular 就會把相應(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 獲得的英雄電話號碼,被投影到上面的英雄描述里,結(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 得到一個緩存服務(wù)。如果該父組件中沒有該服務(wù),Angular 就會拋出錯誤,即使組件樹里的再上級有某個組件擁有這個服務(wù),還是會拋出錯誤。

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

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

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

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

如果你恢復(fù)了 @Host() 裝飾器,并且注釋掉 @Optional 裝飾器,應(yīng)用就會拋出一個錯誤,因為它在宿主組件這一層找不到所需的 Logger。EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

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

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

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òu)造函數(shù)參數(shù),用于為某個依賴提供自定義提供者?,F(xiàn)在,就可以在測試期間使用 localStorage 的 Mock API 來覆蓋這個提供者了,而不必與真實的瀏覽器 API 進(jìn)行交互。

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

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

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

注入組件的 DOM 元素

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

為了說明這一點(diǎn),請看屬性型指令中那個 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è)置為一個高亮顏色。

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

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

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ù),你必須傳給它一個令牌。 Angular 通常會通過指定構(gòu)造函數(shù)參數(shù)以及參數(shù)的類型來處理它。 參數(shù)的類型可以用作注入器的查閱令牌。 Angular 會把該令牌傳給注入器,并把它的結(jié)果賦給相應(yīng)的參數(shù)。

下面是一個典型的例子。

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

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

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

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

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

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

定義提供者

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

它背后的代碼定制了 DI 框架提供依賴項的方法和位置。 這個例子闡明了通過提供對象字面量來把對象的定義和 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ù)組展示了你可以如何使用其它的鍵來定義提供者:useValue、useClass、useExistinguseFactory。

值提供者:useValue

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

HeroOfTheMonthComponent 例子中有兩個值-提供者。

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)有實例,而不是要求注入器使用 new 來創(chuàng)建一個新實例或使用它自己的緩存實例。這里令牌就是這個類本身。

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

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

一個值-提供者的值必須在指定之前定義。 比如標(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');

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

類提供者:useClass

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

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

請看下面 HeroOfTheMonthComponent 里的兩個例子:

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

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

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

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

這個組件及其子組件會得到 DateLoggerService 實例。這個組件樹之外的組件得到的仍是 LoggerService 實例。

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

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

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

{ provide: MinimalLogger, useExisting: LoggerService },

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

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

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;
}

下面的例子在一個簡化版的 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ù)是一個 MinimalLogger 類型,在支持 TypeScript 感知的編輯器里,只能看到它的兩個成員 logslogInfo

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

在下面的圖片中,顯示了日志日期,可以確認(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)還有第三個鍵 deps,它指定了供 useFactory 函數(shù)使用的那些依賴。

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

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

在這個例子中,局部狀態(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ù)需要一個 Hero 和一個 HeroService 參數(shù)。

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

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

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

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

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

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

類-接口

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

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

{ provide: MinimalLogger, useExisting: LoggerService },

MinimalLogger 是一個抽象類。

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;
}

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

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

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

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

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

用類作為接口可以讓你獲得真實 JavaScript 對象中的接口的特性。 但是,為了最小化內(nèi)存開銷,該類應(yīng)該是沒有實現(xiàn)的。 對于構(gòu)造函數(shù),MinimalLogger 會轉(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);

注:

只要不實現(xiàn)它,不管添加多少成員,它都不會增長大小,因為這些成員雖然是有類型的,但卻沒有實現(xiàn)。

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

'InjectionToken' 對象

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

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

InjectionToken 具有這些特征。在Hero of the Month例子中遇見它們兩次,一個是 title 的值,一個是 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á)類型信息。 而且這個令牌的描述信息也可以為開發(fā)者提供幫助。

注入到派生類

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

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

HeroesBaseComponent 能自己獨(dú)立運(yùn)行。它在自己的實例里要求 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)造它們時,無意中做出一些非常戲劇化的動作(比如與服務(wù)器進(jìn)行會話)。 這就是為什么你要在 ngOnInit 里面調(diào)用 HeroService,而不是在構(gòu)造函數(shù)中。

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

可惜,Angular 不能直接在基類里直接注入 HeroService。必須在這個組件里再次提供 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 組件里面建一個 ngOnInit 方法來做排序。但是 Angular 會先調(diào)用派生類的 ngOnInit,后調(diào)用基類的 ngOnInit, 所以可能在英雄到達(dá)之前就開始排序。這就產(chǎn)生了一個討厭的錯誤。

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

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

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

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

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

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

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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號