填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

依賴注入實戰

Dependency injection in action

本章涉及到 Angular 依賴注入(DI)的很多特性。

This guide explores many of the features of dependency injection (DI) in Angular.

要檢視包含本章程式碼片段的可工作範例,參閱現場演練 / 下載範例

See the現場演練 / 下載範例for a working example containing the code snippets in this guide.

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

Multiple service instances (sandboxing)

在元件樹的同一個級別上,有時需要一個服務的多個實例。

Sometimes you want multiple instances of a service at the same level of the component hierarchy.

一個用來儲存其伴生元件的實例狀態的服務就是個好例子。 每個元件都需要該服務的單獨實例。 每個服務有自己的工作狀態,與其它元件的服務和狀態隔離。這叫做沙箱化,因為每個服務和元件實例都在自己的沙箱裡執行。

A good example is a service that holds state for its companion component instance. You need a separate instance of the service for each component. Each service has its own work-state, isolated from the service-and-state of a different component. This is called sandboxing because each service and component instance has its own sandbox to play in.

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

In this example, HeroBiosComponent presents three instances of HeroBioComponent.

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 服務來對該英雄進行讀取、快取和執行其它持久化操作。

Each HeroBioComponent can edit a single hero's biography. HeroBioComponent relies on HeroCacheService to fetch, cache, and perform other persistence operations on that hero.

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 實例。否則它們會相互衝突,爭相把自己的英雄放在快取裡面。

Three instances of HeroBioComponent can't share the same instance of HeroCacheService, as they'd be competing with each other to determine which hero to cache.

它們應該透過在自己的元資料(metadata)providers 數組裡面列出 HeroCacheService, 這樣每個 HeroBioComponent 就能擁有自己獨立的 HeroCacheService 實例了。

Instead, each HeroBioComponent gets its own HeroCacheService instance by listing HeroCacheService in its metadata providers array.

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 = 0;

  constructor(private heroCache: HeroCacheService) { }

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

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

父元件 HeroBiosComponent 把一個值繫結到 heroIdngOnInit 把該 id 傳遞到服務,然後服務獲取和快取英雄。hero 屬性的 getter 從服務裡面獲取快取的英雄,並在範本裡顯示它繫結到屬性值。

The parent HeroBiosComponent binds a value to heroId. ngOnInit passes that ID to the service, which fetches and caches the hero. The getter for the hero property pulls the cached hero from the service. The template displays this data-bound property.

現場演練現場演練 / 下載範例中找到這個例子,確認三個 HeroBioComponent 實例擁有自己獨立的英雄資料快取。

Find this example inlive codelive code / 下載範例and confirm that the three HeroBioComponent instances have their own cached hero data.

使用引數裝飾器來限定依賴查詢方式

Qualify dependency lookup with parameter decorators

當類別需要某個依賴項時,該依賴項就會作為引數新增到類別的建構函式中。 當 Angular 需要實例化該類別時,就會呼叫 DI 框架來提供該依賴。 預設情況下,DI 框架會在注入器樹中查詢一個提供者,從該元件的區域性注入器開始,如果需要,則沿著注入器樹向上冒泡,直到根注入器。

When a class requires a dependency, that dependency is added to the constructor as a parameter. When Angular needs to instantiate the class, it calls upon the DI framework to supply the dependency. By default, the DI framework searches for a provider in the injector hierarchy, starting at the component's local injector of the component, and if necessary bubbling up through the injector tree until it reaches the root injector.

  • 第一個配置過該提供者的注入器就會把依賴(服務實例或值)提供給這個建構函式。

    The first injector configured with a provider supplies the dependency (a service instance or value) to the constructor.

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

    If no provider is found in the root injector, the DI framework throws an error.

透過在類別的建構函式中對服務引數使用引數裝飾器,可以提供一些選項來修改預設的搜尋行為。

There are a number of options for modifying the default search behavior, using parameter decorators on the service-valued parameters of a class constructor.

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

Make a dependency @Optional and limit search with @Host

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

Dependencies can be registered at any level in the component hierarchy. When a component requests a dependency, Angular starts with that component's injector and walks up the injector tree until it finds the first suitable provider. Angular throws an error if it can't find the dependency during that walk.

某些情況下,你需要限制搜尋,或容忍依賴項的缺失。 你可以使用元件建構函式引數上的 @Host@Optional 這兩個限定裝飾器來修改 Angular 的搜尋行為。

In some cases, you need to limit the search or accommodate a missing dependency. You can modify Angular's search behavior with the @Host and @Optional qualifying decorators on a service-valued parameter of the component's constructor.

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

    The @Optional property decorator tells Angular to return null when it can't find the dependency.

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

    The @Host property decorator stops the upward search at the host component. The host component is typically the component requesting the dependency. However, when this component is projected into a parent component, that parent component becomes the host. The following example covers this second case.

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

These decorators can be used individually or together, as shown in the example. This HeroBiosAndContactsComponent is a revision of HeroBiosComponent which you looked at above.

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

注意看範本:

Focus on the template:

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> 標籤中是一個新的 <hero-contact> 元素。Angular 就會把相應的 HeroContactComponent投影(transclude)進 HeroBioComponent 的視圖裡, 將它放在 HeroBioComponent 範本的 <ng-content> 標籤槽裡。

Now there's a new <hero-contact> element between the <hero-bio> tags. Angular projects, or transcludes, the corresponding HeroContactComponent into the HeroBioComponent view, placing it in the <ng-content> slot of the HeroBioComponent template.

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

HeroContactComponent 獲得的英雄電話號碼,被投影到上面的英雄描述裡,結果如下:

The result is shown below, with the hero's telephone number from HeroContactComponent projected above the hero description.

這裡的 HeroContactComponent 示範了限定型裝飾器。

Here's HeroContactComponent, which demonstrates the qualifying decorators.

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

}
    

注意建構函式的引數。

Focus on the constructor parameters.

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() 函式是建構函式屬性 heroCache 的裝飾器,確保從其父元件 HeroBioComponent 得到一個快取服務。如果該父元件中沒有該服務,Angular 就會丟擲錯誤,即使元件樹裡的再上級有某個元件擁有這個服務,還是會丟擲錯誤。

The @Host() function decorating the heroCache constructor property ensures that you get a reference to the cache service from the parent HeroBioComponent. Angular throws an error if the parent lacks that service, even if a component higher in the component tree includes it.

另一個 @Host() 函式是建構函式屬性 loggerService 的裝飾器。 在本應用程式中只有一個在 AppComponent 級提供的 LoggerService 實例。 該宿主 HeroBioComponent 沒有自己的 LoggerService 提供者。

A second @Host() function decorates the loggerService constructor property. The only LoggerService instance in the app is provided at the AppComponent level. The host HeroBioComponent doesn't have its own LoggerService provider.

如果沒有同時使用 @Optional() 裝飾器的話,Angular 就會丟擲錯誤。當該屬性帶有 @Optional() 標記時,Angular 就會把 loggerService 設定為 null,並繼續執行元件而不會丟擲錯誤。

Angular throws an error if you haven't also decorated the property with @Optional(). When the property is marked as optional, Angular sets loggerService to null and the rest of the component adapts.

下面是 HeroBiosAndContactsComponent 的執行結果:

Here's HeroBiosAndContactsComponent in action.

如果註釋掉 @Host() 裝飾器,Angular 就會沿著注入器樹往上走,直到在 AppComponent 中找到該日誌服務。日誌服務的邏輯加了進來,所顯示的英雄資訊增加了 "!!!" 標記,這表明確實找到了日誌服務。

If you comment out the @Host() decorator, Angular walks up the injector ancestor tree until it finds the logger at the AppComponent level. The logger logic kicks in and the hero display updates with the "!!!" marker to indicate that the logger was found.

如果你恢復了 @Host() 裝飾器,並且註釋掉 @Optional 裝飾器,應用就會丟擲一個錯誤,因為它在宿主元件這一層找不到所需的 LoggerEXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

If you restore the @Host() decorator and comment out @Optional, the app throws an exception when it cannot find the required logger at the host component level. EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)

使用 @Inject 指定自訂提供者

Supply a custom provider with @Inject

自訂提供者讓你可以為隱式依賴提供一個具體的實現,比如內建瀏覽器 API。下面的例子使用 InjectionToken 來提供 localStorage,將其作為 BrowserStorageService 的依賴項。

Using a custom provider allows you to provide a concrete implementation for implicit dependencies, such as built-in browser APIs. The following example uses an InjectionToken to provide the localStorage browser API as a dependency in the BrowserStorageService.

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) {
    return 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 函式返回 window 物件上的 localStorage 屬性。Inject 裝飾器修飾一個建構函式引數,用於為某個依賴提供自訂提供者。現在,就可以在測試期間使用 localStorage 的 Mock API 來覆蓋這個提供者了,而不必與真實的瀏覽器 API 進行互動。

The factory function returns the localStorage property that is attached to the browser window object. The Inject decorator is a constructor parameter used to specify a custom provider of a dependency. This custom provider can now be overridden during testing with a mock API of localStorage instead of interacting with real browser APIs.

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

Modify the provider search with @Self and @SkipSelf

注入器也可以透過建構函式的引數裝飾器來指定範圍。下面的例子就在 Component 類別的 providers 中使用瀏覽器的 sessionStorage API 覆蓋了 BROWSER_STORAGE 令牌。同一個 BrowserStorageService 在建構函式中使用 @Self@SkipSelf 裝飾器注入了兩次,來分別指定由哪個注入器來提供依賴。

Providers can also be scoped by injector through constructor parameter decorators. The following example overrides the BROWSER_STORAGE token in the Component class providers with the sessionStorage browser API. The same BrowserStorageService is injected twice in the constructor, decorated with @Self and @SkipSelf to define which injector handles the provider dependency.

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 裝飾器可以讓你跳過區域性注入器,並在注入器樹中向上查詢,以發現哪個提供者滿足該依賴。 sessionStorageService 實例使用瀏覽器的 sessionStorage 來跟 BrowserStorageService 打交道,而 localStorageService 跳過了區域性注入器,使用根注入器提供的 BrowserStorageService,它使用瀏覽器的 localStorage API。

Using the @Self decorator, the injector only looks at the component's injector for its providers. The @SkipSelf decorator allows you to skip the local injector and look up in the hierarchy to find a provider that satisfies this dependency. The sessionStorageService instance interacts with the BrowserStorageService using the sessionStorage browser API, while the localStorageService skips the local injector and uses the root BrowserStorageService that uses the localStorage browser API.

注入元件的 DOM 元素

Inject the component's DOM element

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

Although developers strive to avoid it, many visual effects and third-party tools, such as jQuery, require DOM access. As a result, you might need to access a component's DOM element.

為了說明這一點,請看屬性型指令中那個 HighlightDirective 的簡化版。

To illustrate, here's a minimal version of HighlightDirective from the Attribute Directives page.

src/app/highlight.directive.ts
      
      import { Directive, ElementRef, HostListener, Input } from '@angular/core';

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

  @Input('appHighlight') highlightColor = '';

  private el: HTMLElement;

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

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

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

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

當用戶把滑鼠移到 DOM 元素上時,指令將指令所在的元素的背景設定為一個高亮顏色。

The directive sets the background to a highlight color when the user mouses over the DOM element to which the directive is applied.

Angular 把建構函式引數 el 設定為注入的 ElementRef,該 ElementRef 代表了宿主的 DOM 元素,它的 nativeElement 屬性把該 DOM 元素暴露給了指令。

Angular sets the constructor's el parameter to the injected ElementRef. (An ElementRef is a wrapper around a DOM element, whose nativeElement property exposes the DOM element for the directive to manipulate.)

下面的程式碼把指令的 appHighlight 屬性(Attribute)填加到兩個 <div> 標籤裡,一個沒有賦值,一個賦值了顏色。

The sample code applies the directive's appHighlight attribute to two <div> tags, first without a value (yielding the default color) and then with an assigned color value.

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>
    

下圖顯示了滑鼠移到 <hero-bios-and-contacts> 標籤上的效果:

The following image shows the effect of mousing over the <hero-bios-and-contacts> tag.

定義提供者

Defining providers

用於實例化類別的預設方法不一定總適合用來建立依賴。你可以到依賴提供者部分檢視其它方法。 HeroOfTheMonthComponent 例子示範了一些替代方案,展示了為什麼需要它們。 它看起來很簡單:一些屬性和一些由 logger 產生的日誌。

A dependency can't always be created by the default method of instantiating a class. You learned about some other methods in Dependency Providers. The following HeroOfTheMonthComponent example demonstrates many of the alternatives and why you need them. It's visually simple: a few properties and the logs produced by a logger.

它背後的程式碼訂製了 DI 框架提供依賴項的方法和位置。 這個例子闡明瞭透過提供物件字面量來把物件的定義和 DI 令牌關聯起來的另一種方式。

The code behind it customizes how and where the DI framework provides dependencies. The use cases illustrate different ways to use the provide object literal to associate a definition object with a DI token.

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 陣列展示了你可以如何使用其它的鍵來定義提供者:useValueuseClassuseExistinguseFactory

The providers array shows how you might use the different provider-definition keys; useValue, useClass, useExisting, or useFactory.

值提供者:useValue

Value providers: useValue

useValue 鍵讓你可以為 DI 令牌關聯一個固定的值。 使用該技巧來進行執行期常量設定,比如網站的基礎地址和功能標誌等。 你也可以在單元測試中使用值提供者,來用一個 Mock 資料來代替一個生產環境下的資料服務。

The useValue key lets you associate a fixed value with a DI token. Use this technique to provide runtime configuration constants such as website base addresses and feature flags. You can also use a value provider in a unit test to provide mock data in place of a production data service.

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

The HeroOfTheMonthComponent example has two value providers.

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 類別的現有實例,而不是要求注入器使用 new 來建立一個新實例或使用它自己的快取實例。這裡令牌就是這個類別本身。

    The first provides an existing instance of the Hero class to use for the Hero token, rather than requiring the injector to create a new instance with new or use its own cached instance. Here, the token is the class itself.

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

    The second specifies a literal string resource to use for the TITLE token. The TITLE provider token is not a class, but is instead a special kind of provider lookup key called an injection token, represented by an InjectionToken instance.

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

You can use an injection token for any kind of provider but it's particularly helpful when the dependency is a simple value like a string, a number, or a function.

一個值-提供者的值必須在指定之前定義。 比如標題字串就是立即可用的。 該例中的 someHero 變數是以前在如下的檔案中定義的。 你不能使用那些要等以後才能定義其值的變數。

The value of a value provider must be defined before you specify it here. The title string literal is immediately available. The someHero variable in this example was set earlier in the file as shown below. You can't use a variable whose value will be defined later.

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');
    

其它型別的提供者都會惰性建立它們的值,也就是說只在需要注入它們的時候才建立。

Other types of providers can create their values lazily; that is, when they're needed for injection.

類別提供者:useClass

Class providers: useClass

useClass 提供的鍵讓你可以建立並返回指定類別的新實例。

The useClass provider key lets you create and return a new instance of the specified class.

你可以使用這類別提供者來為公共類別或預設類別換上一個替代實現。比如,這個替代實現可以實現一種不同的策略來擴充套件預設類別,或在測試環境中模擬真實類別的行為。

You can use this type of provider to substitute an alternative implementation for a common or default class. The alternative implementation could, for example, implement a different strategy, extend the default class, or emulate the behavior of the real class in a test case.

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

The following code shows two examples in HeroOfTheMonthComponent.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: HeroService,   useClass:    HeroService },
{ provide: LoggerService, useClass:    DateLoggerService },
    

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

The first provider is the de-sugared, expanded form of the most typical case in which the class to be created (HeroService) is also the provider's dependency injection token. The short form is generally preferred; this long form makes the details explicit.

第二個提供者使用 DateLoggerService 來滿足 LoggerService。該 LoggerServiceAppComponent 級別已經被註冊。當這個元件要求 LoggerService 的時候,它得到的卻是 DateLoggerService 服務的實例。

The second provider substitutes DateLoggerService for LoggerService. LoggerService is already registered at the AppComponent level. When this child component requests LoggerService, it receives a DateLoggerService instance instead.

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

This component and its tree of child components receive DateLoggerService instance. Components outside the tree continue to receive the original LoggerService instance.

DateLoggerServiceLoggerService 繼承;它把當前的日期/時間附加到每條資訊上。

DateLoggerService inherits from LoggerService; it appends the current date/time to each message:

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

Alias providers: useExisting

useExisting 提供了一個鍵,讓你可以把一個令牌對映成另一個令牌。實際上,第一個令牌就是第二個令牌所關聯的服務的別名,這樣就建立了訪問同一個服務物件的兩種途徑。

The useExisting provider key lets you map one token to another. In effect, the first token is an alias for the service associated with the second token, creating two ways to access the same service object.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: MinimalLogger, useExisting: LoggerService },
    

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

You can use this technique to narrow an API through an aliasing interface. The following example shows an alias introduced for that purpose.

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

Imagine that LoggerService had a large API, much larger than the actual three methods and a property. You might want to shrink that API surface to just the members you actually need. In this example, the MinimalLogger class-interface reduces the API to two members:

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 {
  abstract logs: string[];
  abstract logInfo: (msg: string) => void;
}
    

下面的例子在一個簡化版的 HeroOfTheMonthComponent 中使用 MinimalLogger

The following example puts MinimalLogger to use in a simplified version of HeroOfTheMonthComponent.

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 建構函式的 logger 引數是一個 MinimalLogger 型別,在支援 TypeScript 感知的編輯器裡,只能看到它的兩個成員 logslogInfo

The HeroOfTheMonthComponent constructor's logger parameter is typed as MinimalLogger, so only the logs and logInfo members are visible in a TypeScript-aware editor.

實際上,Angular 把 logger 引數設定為注入器裡 LoggerService 令牌下注冊的完整服務,該令牌恰好是以前提供的那個 DateLoggerService 實例。

Behind the scenes, Angular sets the logger parameter to the full service registered under the LoggingService token, which happens to be the DateLoggerService instance that was provided above.

在下面的圖片中,顯示了日誌日期,可以確認這一點:

This is illustrated in the following image, which displays the logging date.

工廠提供者:useFactory

Factory providers: useFactory

useFactory 提供了一個鍵,讓你可以透過呼叫一個工廠函式來建立依賴實例,如下面的例子所示。

The useFactory provider key lets you create a dependency object by calling a factory function, as in the following example.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
    

注入器透過呼叫你用 useFactory 鍵指定的工廠函式來提供該依賴的值。 注意,提供者的這種形態還有第三個鍵 deps,它指定了供 useFactory 函式使用的那些依賴。

The injector provides the dependency value by invoking a factory function, that you provide as the value of the useFactory key. Notice that this form of provider has a third key, deps, which specifies dependencies for the useFactory function.

使用這項技術,可以用包含了一些依賴服務和本地狀態輸入的工廠函式來建立一個依賴物件

Use this technique to create a dependency object with a factory function whose inputs are a combination of injected services and local state.

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

The dependency object (returned by the factory function) is typically a class instance, but can be other things as well. In this example, the dependency object is a string of the names of the runners up to the "Hero of the Month" contest.

在這個例子中,區域性狀態是數字 2,也就是元件應該顯示的參賽者數量。 該狀態的值傳給了 runnersUpFactory() 作為引數。 runnersUpFactory() 返回了提供者的工廠函式,它可以使用傳入的狀態值和注入的服務 HeroHeroService

In the example, the local state is the number 2, the number of runners up that the component should show. The state value is passed as an argument to runnersUpFactory(). The runnersUpFactory() returns the provider factory function, which can use both the passed-in state value and the injected services Hero and HeroService.

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

runnersUpFactory() 返回的提供者的工廠函式返回了實際的依賴物件,也就是表示名字的字串。

The provider factory function (returned by runnersUpFactory()) returns the actual dependency object, the string of names.

  • 這個返回的函式需要一個 Hero 和一個 HeroService 引數。

    The function takes a winning Hero and a HeroService as arguments.

Angular 根據 deps 陣列中指定的兩個令牌來提供這些注入引數。

Angular supplies these arguments from injected values identified by the two tokens in the deps array.

  • 該函式返回名字的字串,Angular 可以把它們注入到 HeroOfTheMonthComponentrunnersUp 引數中。

    The function returns the string of names, which Angular than injects into the runnersUp parameter of HeroOfTheMonthComponent.

該函式從 HeroService 中接受候選的英雄,從中取 2 個參加競賽,並把他們的名字串接起來返回。 參閱現場演練 / 下載範例檢視完整原始碼。

The function retrieves candidate heroes from the HeroService, takes 2 of them to be the runners-up, and returns their concatenated names. Look at the現場演練 / 下載範例for the full source code.

提供替代令牌:類別介面與 'InjectionToken'

Provider token alternatives: class interface and 'InjectionToken'

當使用類別作為令牌,同時也把它作為返回依賴物件或服務的型別時,Angular 依賴注入使用起來最容易。

Angular dependency injection is easiest when the provider token is a class that is also the type of the returned dependency object, or service.

但令牌不一定都是類別,就算它是一個類別,它也不一定都返回型別相同的物件。這是下一節的主題。

However, a token doesn't have to be a class and even when it is a class, it doesn't have to be the same type as the returned object. That's the subject of the next section.

類別-介面

Classinterface

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

The previous Hero of the Month example used the MinimalLogger class as the token for a provider of LoggerService.

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      { provide: MinimalLogger, useExisting: LoggerService },
    

MinimalLogger 是一個抽象類別。

MinimalLogger is an abstract class.

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 {
  abstract logs: string[];
  abstract logInfo: (msg: string) => void;
}
    

你通常從一個可擴充套件的抽象類別繼承。但這個應用中並沒有類別會繼承 MinimalLogger

An abstract class is usually a base class that you can extend. In this app, however there is no class that inherits from MinimalLogger.

LoggerServiceDateLoggerService本可以MinimalLogger 中繼承。 它們也可以實現 MinimalLogger,而不用單獨定義介面。 但它們沒有。 MinimalLogger 在這裡僅僅被用作一個 "依賴注入令牌"。

The LoggerService and the DateLoggerService could have inherited from MinimalLogger, or they could have implemented it instead, in the manner of an interface. But they did neither. MinimalLogger is used only as a dependency injection token.

當你透過這種方式使用類別時,它稱作類別介面

When you use a class this way, it's called a class interface.

就像 DI 提供者中提到的那樣,介面不是有效的 DI 令牌,因為它是 TypeScript 自己用的,在執行期間不存在。使用這種抽象類別介面不但可以獲得像介面一樣的強型別,而且可以像普通類別一樣把它用作提供者令牌。

As mentioned in DI Providers, an interface is not a valid DI token because it is a TypeScript artifact that doesn't exist at run time. Use this abstract class interface to get the strong typing of an interface, and also use it as a provider token in the way you would a normal class.

類別介面應該定義允許它的消費者呼叫的成員。窄的介面有助於解耦該類別的具體實現和它的消費者。

A class interface should define only the members that its consumers are allowed to call. Such a narrowing interface helps decouple the concrete class from its consumers.

用類別作為介面可以讓你獲得真實 JavaScript 物件中的介面的特性。 但是,為了最小化記憶體開銷,該類別應該是沒有實現的。 對於建構函式,MinimalLogger 會轉譯成未優化過的、預先最小化過的 JavaScript。

Using a class as an interface gives you the characteristics of an interface in a real JavaScript object. To minimize memory cost, however, the class should have no implementation. The MinimalLogger transpiles to this unoptimized, pre-minified JavaScript for a constructor function.

dependency-injection-in-action/src/app/minimal-logger.service.ts
      
      var MinimalLogger = (function () {
  function MinimalLogger() {}
  return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
    

注意,只要不實現它,不管新增多少成員,它都不會增長大小,因為這些成員雖然是有型別的,但卻沒有實現。

Notice that it doesn't have any members. It never grows no matter how many members you add to the class, as long as those members are typed but not implemented.

你可以再看看 TypeScript 的 MinimalLogger 類別,確定一下它是沒有實現的。

Look again at the TypeScript MinimalLogger class to confirm that it has no implementation.

'InjectionToken' 物件

'InjectionToken' objects

依賴物件可以是一個簡單的值,比如日期,數字和字串,或者一個無形的物件,比如陣列和函式。

Dependency objects can be simple values like dates, numbers and strings, or shapeless objects like arrays and functions.

這樣的物件沒有應用程式介面,所以不能用一個類別來表示。更適合表示它們的是:唯一的和符號性的令牌,一個 JavaScript 物件,擁有一個友好的名字,但不會與其它的同名令牌發生衝突。

Such objects don't have application interfaces and therefore aren't well represented by a class. They're better represented by a token that is both unique and symbolic, a JavaScript object that has a friendly name but won't conflict with another token that happens to have the same name.

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

InjectionToken has these characteristics. You encountered them twice in the Hero of the Month example, in the title value provider and in the runnersUp factory provider.

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

這樣建立 TITLE 令牌:

You created the TITLE token like this:

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
      
      import { InjectionToken } from '@angular/core';

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

型別引數,雖然是可選的,但可以向開發者和開發工具傳達型別資訊。 而且這個令牌的描述資訊也可以為開發者提供幫助。

The type parameter, while optional, conveys the dependency's type to developers and tooling. The token description is another developer aid.

注入到派生類別

Inject into a derived class

當編寫一個繼承自另一個元件的元件時,要格外小心。如果基礎元件有依賴注入,必須要在派生類別中重新提供和重新注入它們,並將它們透過建構函式傳給基底類別。

Take care when writing a component that inherits from another component. If the base component has injected dependencies, you must re-provide and re-inject them in the derived class and then pass them down to the base class through the constructor.

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

In this contrived example, SortedHeroesComponent inherits from HeroesBaseComponent to display a sorted list of heroes.

HeroesBaseComponent 能自己獨立執行。它在自己的實例裡要求 HeroService,用來得到英雄,並將他們按照資料庫返回的順序顯示出來。

The HeroesBaseComponent can stand on its own. It demands its own instance of HeroService to get heroes and displays them in the order they arrive from the database.

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: Hero[] = [];

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

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

}
    

讓建構函式保持簡單

Keep constructors simple

建構函式應該只用來初始化變數。 這條規則讓元件在測試環境中可以放心地構造元件,以免在構造它們時,無意中做出一些非常戲劇化的動作(比如與伺服器進行會話)。 這就是為什麼你要在 ngOnInit 裡面呼叫 HeroService,而不是在建構函式中。

Constructors should do little more than initialize variables. This rule makes the component safe to construct under test without fear that it will do something dramatic like talk to the server. That's why you call the HeroService from within the ngOnInit rather than the constructor.

使用者希望看到英雄按字母順序排序。與其修改原始的元件,不如派生它,新建 SortedHeroesComponent,以便展示英雄之前進行排序。 SortedHeroesComponent 讓基底類別來獲取英雄。

Users want to see the heroes in alphabetical order. Rather than modify the original component, sub-class it and create a SortedHeroesComponent that sorts the heroes before presenting them. The SortedHeroesComponent lets the base class fetch the heroes.

可惜,Angular 不能直接在基底類別裡直接注入 HeroService。必須在這個元件裡再次提供 HeroService,然後透過建構函式傳給基底類別。

Unfortunately, Angular cannot inject the HeroService directly into the base class. You must provide the HeroService again for this component, then pass it down to the base class inside the constructor.

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

現在,請注意 afterGetHeroes() 方法。 你的第一反應是在 SortedHeroesComponent 元件裡面建一個 ngOnInit 方法來做排序。但是 Angular 會先呼叫派生類別的 ngOnInit,後呼叫基底類別的 ngOnInit, 所以可能在英雄到達之前就開始排序。這就產生了一個討厭的錯誤。

Now take note of the afterGetHeroes() method. Your first instinct might have been to create an ngOnInit method in SortedHeroesComponent and do the sorting there. But Angular calls the derived class's ngOnInit before calling the base class's ngOnInit so you'd be sorting the heroes array before they arrived. That produces a nasty error.

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

Overriding the base class's afterGetHeroes() method solves the problem.

分析上面的這些複雜性是為了強調避免使用元件繼承這一點。

These complications argue for avoiding component inheritance.

使用一個前向參考(forwardRef)來打破迴圈

Break circularities with a forward class reference (forwardRef)

在 TypeScript 裡面,類別宣告的順序是很重要的。如果一個類別尚未定義,就不能參考它。

The order of class declaration matters in TypeScript. You can't refer directly to a class until it's been defined.

這通常不是一個問題,特別是當你遵循一個檔案一個類別規則的時候。 但是有時候迴圈參考可能不能避免。當一個類別A 參考類別 B,同時'B'參考'A'的時候,你就陷入困境了:它們中間的某一個必須要先定義。

This isn't usually a problem, especially if you adhere to the recommended one class per file rule. But sometimes circular references are unavoidable. You're in a bind when class 'A' refers to class 'B' and 'B' refers to 'A'. One of them has to be defined first.

Angular 的 forwardRef() 函式建立一個間接地參考,Angular 可以隨後解析。

The Angular forwardRef() function creates an indirect reference that Angular can resolve later.

這個關於父查詢器的例子中全都是沒辦法打破的迴圈類別參考。

The Parent Finder sample is full of circular class references that are impossible to break.

當一個類別需要參考自身的時候,你面臨同樣的困境,就像在 AlexComponentprovdiers 陣列中遇到的困境一樣。 該 providers 陣列是一個 @Component() 裝飾器函式的一個屬性,它必須在類別定義之前出現。

You face this dilemma when a class makes a reference to itself as does AlexComponent in its providers array. The providers array is a property of the @Component() decorator function which must appear above the class definition.

使用 forwardRef 來打破這種迴圈:

Break the circularity with forwardRef.

parent-finder.component.ts (AlexComponent providers)
      
      providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],