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

Angular 中的依賴注入

Dependency injection in Angular

依賴注入(DI)是一種重要的應用設計模式。 Angular 有自己的 DI 框架,在設計應用時常會用到它,以提升它們的開發效率和模組化程度。

Dependency injection (DI), is an important application design pattern. Angular has its own DI framework, which is typically used in the design of Angular applications to increase their efficiency and modularity.

依賴,是當類別需要執行其功能時,所需要的服務或物件。 DI 是一種編碼模式,其中的類別會從外部源中請求獲取依賴,而不是自己建立它們。

Dependencies are services or objects that a class needs to perform its function. DI is a coding pattern in which a class asks for dependencies from external sources rather than creating them itself.

在 Angular 中,DI 框架會在實例化該類別時向其提供這個類別所宣告的依賴項。本指南介紹了 DI 在 Angular 中的工作原理,以及如何藉助它來讓你的應用更靈活、高效、健壯,以及可測試、可維護。

In Angular, the DI framework provides declared dependencies to a class when that class is instantiated. This guide explains how DI works in Angular, and how you use it to make your apps flexible, efficient, and robust, as well as testable and maintainable.

你可以執行本章這個範例應用的現場演練 / 下載範例

You can run the現場演練 / 下載範例of the sample app that accompanies this guide.

我們先看一下英雄之旅英雄管理特性的簡化版。這個簡化版不使用 DI,我們將逐步把它轉換成使用 DI 的。

Start by reviewing this simplified version of the heroes feature from the The Tour of Heroes. This simple version doesn't use DI; we'll walk through converting it to do so.

import { Component } from '@angular/core'; @Component({ selector: 'app-heroes', template: ` <h2>Heroes</h2> <app-hero-list></app-hero-list> ` }) export class HeroesComponent { }import { Component } from '@angular/core'; import { HEROES } from './mock-heroes'; @Component({ selector: 'app-hero-list', template: ` <div *ngFor="let hero of heroes"> {{hero.id}} - {{hero.name}} </div> ` }) export class HeroListComponent { heroes = HEROES; }export interface Hero { id: number; name: string; isSecret: boolean; }import { Hero } from './hero'; export const HEROES: Hero[] = [ { id: 11, isSecret: false, name: 'Dr Nice' }, { id: 12, isSecret: false, name: 'Narco' }, { id: 13, isSecret: false, name: 'Bombasto' }, { id: 14, isSecret: false, name: 'Celeritas' }, { id: 15, isSecret: false, name: 'Magneta' }, { id: 16, isSecret: false, name: 'RubberMan' }, { id: 17, isSecret: false, name: 'Dynama' }, { id: 18, isSecret: true, name: 'Dr IQ' }, { id: 19, isSecret: true, name: 'Magma' }, { id: 20, isSecret: true, name: 'Tornado' } ];
      
      import { Component } from '@angular/core';

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

HeroesComponent 是最上層英雄管理元件。 它唯一的目的是顯示 HeroListComponent,該元件會顯示一個英雄名字的列表。

HeroesComponent is the top-level heroes component. Its only purpose is to display HeroListComponent, which displays a list of hero names.

HeroListComponent 的這個版本從 HEROES 陣列(它在一個獨立的 mock-heroes 檔案中定義了一個記憶體集合)中獲取英雄。

This version of the HeroListComponent gets heroes from the HEROES array, an in-memory collection defined in a separate mock-heroes file.

export class HeroListComponent { heroes = HEROES; }
src/app/heroes/hero-list.component.ts (class)
      
      export class HeroListComponent {
  heroes = HEROES;
}
    

這種方法在原型階段有用,但是不夠健壯、不利於維護。 一旦你想要測試該元件或想從遠端伺服器獲得英雄列表,就不得不修改 HeroesListComponent 的實現,並且替換每一處使用了 HEROES 模擬資料的地方。

This approach works for prototyping, but is not robust or maintainable. As soon as you try to test this component or get heroes from a remote server, you have to change the implementation of HeroesListComponent and replace every use of the HEROES mock data.

建立和註冊可注入的服務

Create and register an injectable service

DI 框架讓你能從一個可注入的服務類別(獨立檔案)中為元件提供資料。為了示範,我們還會建立一個用來提供英雄列表的、可注入的服務類別,並把它註冊為該服務的提供者。

The DI framework lets you supply data to a component from an injectable service class, defined in its own file. To demonstrate, we'll create an injectable service class that provides a list of heroes, and register that class as a provider of that service.

在同一個檔案中放多個類別容易讓人困惑。我們通常建議你在單獨的檔案中定義元件和服務。

Having multiple classes in the same file can be confusing. We generally recommend that you define components and services in separate files.

如果你把元件和服務都放在同一個檔案中,請務必先定義服務,然後再定義元件。如果在服務之前定義元件,則會在執行時收到一個空參考錯誤。

If you do combine a component and service in the same file, it is important to define the service first, and then the component. If you define the component before the service, you get a run-time null reference error.

也可以藉助 forwardRef() 方法來先定義元件,就像這個部落格中解釋的那樣。

It is possible to define the component first with the help of the forwardRef() method as explained in this blog post.

你還可以使用前向參考來打破迴圈依賴,參閱 DI 一章中的例子。

You can also use forward references to break circular dependencies. See an example in the DI Cookbook.

建立可注入的服務類別

Create an injectable service class

Angular CLI 可以用下列命令在 src/app/heroes 目錄下產生一個新的 HeroService 類別。

The Angular CLI can generate a new HeroService class in the src/app/heroes folder with this command.

ng generate service heroes/hero
      
      ng generate service heroes/hero
    

下列命令會建立 HeroService 的骨架。

The command creates the following HeroService skeleton.

import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class HeroService { constructor() { } }
src/app/heroes/hero.service.ts (CLI-generated)
      
      import { Injectable } from '@angular/core';

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

@Injectable() 是每個 Angular 服務定義中的基本要素。該類別的其餘部分匯出了一個 getHeroes 方法,它會返回像以前一樣的模擬資料。(真實的應用可能會從遠端伺服器中非同步獲取這些資料,不過這裡我們先忽略它,專心實現服務的注入機制。)

The @Injectable() is an essential ingredient in every Angular service definition. The rest of the class has been written to expose a getHeroes method that returns the same mock data as before. (A real app would probably get its data asynchronously from a remote server, but we'll ignore that to focus on the mechanics of injecting the service.)

import { Injectable } from '@angular/core'; import { HEROES } from './mock-heroes'; @Injectable({ // we declare that this service should be created // by the root application injector. providedIn: 'root', }) export class HeroService { getHeroes() { return HEROES; } }
src/app/heroes/hero.service.ts
      
      import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';

@Injectable({
  // we declare that this service should be created
  // by the root application injector.
  providedIn: 'root',
})
export class HeroService {
  getHeroes() { return HEROES; }
}
    

用服務提供者配置注入器

Configure an injector with a service provider

我們建立的類別提供了一個服務。@Injectable() 裝飾器把它標記為可供注入的服務,不過在你使用該服務的 provider 提供者配置好 Angular 的依賴注入器之前,Angular 實際上無法將其注入到任何位置。

The class we have created provides a service. The @Injectable() decorator marks it as a service that can be injected, but Angular can't actually inject it anywhere until you configure an Angular dependency injector with a provider of that service.

該注入器負責建立服務實例,並把它們注入到像 HeroListComponent 這樣的類別中。 你很少需要自己建立 Angular 的注入器。Angular 會在執行應用時為你建立注入器,第一個注入器是根注入器,創建於啟動過程中。

The injector is responsible for creating service instances and injecting them into classes like HeroListComponent. You rarely create an Angular injector yourself. Angular creates injectors for you as it executes the app, starting with the root injector that it creates during the bootstrap process.

提供者會告訴注入器如何建立該服務。 要想讓注入器能夠建立服務(或提供其它型別的依賴),你必須使用某個提供者配置好注入器。

A provider tells an injector how to create the service. You must configure an injector with a provider before that injector can create a service (or provide any other kind of dependency).

提供者可以是服務類別本身,因此注入器可以使用 new 來建立實例。 你還可以定義多個類別,以不同的方式提供同一個服務,並使用不同的提供者來配置不同的注入器。

A provider can be the service class itself, so that the injector can use new to create an instance. You might also define more than one class to provide the same service in different ways, and configure different injectors with different providers.

注入器是可繼承的,這意味著如果指定的注入器無法解析某個依賴,它就會請求父注入器來解析它。 元件可以從它自己的注入器來獲取服務、從其祖先元件的注入器中獲取、從其父 NgModule 的注入器中獲取,或從 root 注入器中獲取。

Injectors are inherited, which means that if a given injector can't resolve a dependency, it asks the parent injector to resolve it. A component can get services from its own injector, from the injectors of its component ancestors, from the injector of its parent NgModule, or from the root injector.

你可以在三種位置之一設定元資料,以便在應用的不同層級使用提供者來配置注入器:

You can configure injectors with providers at different levels of your app, by setting a metadata value in one of three places:

  • 在服務本身的 @Injectable() 裝飾器中。

    In the @Injectable() decorator for the service itself.

  • 在 NgModule 的 @NgModule() 裝飾器中。

    In the @NgModule() decorator for an NgModule.

  • 在元件的 @Component() 裝飾器中。

    In the @Component() decorator for a component.

@Injectable() 裝飾器具有一個名叫 providedIn 的元資料選項,在那裡你可以指定把被裝飾類別的提供者放到 root 注入器中,或某個特定 NgModule 的注入器中。

The @Injectable() decorator has the providedIn metadata option, where you can specify the provider of the decorated service class with the root injector, or with the injector for a specific NgModule.

@NgModule()@Component() 裝飾器都有用一個 providers 元資料選項,在那裡你可以配置 NgModule 級或元件級的注入器。

The @NgModule() and @Component() decorators have the providers metadata option, where you can configure providers for NgModule-level or component-level injectors.

所有元件都是指令,而 providers 選項是從 @Directive() 中繼承來的。 你也可以與元件一樣的級別為指令、管道配置提供者。

Components are directives, and the providers option is inherited from @Directive(). You can also configure providers for directives and pipes at the same level as the component.

欲知詳情,參閱該在哪裡配置提供者

Learn more about where to configure providers.

注入服務

Injecting services

HeroListComponent 要想從 HeroService 中獲取英雄列表,就得要求注入 HeroService,而不是自己使用 new 來建立自己的 HeroService 實例。

In order for HeroListComponent to get heroes from HeroService, it needs to ask for HeroService to be injected, rather than creating its own HeroService instance with new.

你可以透過制定帶有依賴型別的建構函式引數來要求 Angular 在元件的建構函式中注入依賴項。下面的程式碼是 HeroListComponent 的建構函式,它要求注入 HeroService

You can tell Angular to inject a dependency in a component's constructor by specifying a constructor parameter with the dependency type. Here's the HeroListComponent constructor, asking for the HeroService to be injected.

constructor(heroService: HeroService)
src/app/heroes/hero-list.component (constructor signature)
      
      constructor(heroService: HeroService)
    

當然,HeroListComponent 還應該使用注入的這個 HeroService 做一些事情。 這裡是修改過的元件,它轉而使用注入的服務。與前一版本並列顯示,以便比較。

Of course, HeroListComponent should do something with the injected HeroService. Here's the revised component, making use of the injected service, side-by-side with the previous version for comparison.

import { Component } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'app-hero-list', template: ` <div *ngFor="let hero of heroes"> {{hero.id}} - {{hero.name}} </div> ` }) export class HeroListComponent { heroes: Hero[]; constructor(heroService: HeroService) { this.heroes = heroService.getHeroes(); } }import { Component } from '@angular/core'; import { HEROES } from './mock-heroes'; @Component({ selector: 'app-hero-list', template: ` <div *ngFor="let hero of heroes"> {{hero.id}} - {{hero.name}} </div> ` }) export class HeroListComponent { heroes = HEROES; }
      
      import { Component } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'app-hero-list',
  template: `
    <div *ngFor="let hero of heroes">
      {{hero.id}} - {{hero.name}}
    </div>
  `
})
export class HeroListComponent {
  heroes: Hero[];

  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }
}
    

必須在某些父注入器中提供 HeroServiceHeroListComponent 並不關心 HeroService 來自哪裡。 如果你決定在 AppModule 中提供 HeroService,也不必修改 HeroListComponent

HeroService must be provided in some parent injector. The code in HeroListComponent doesn't depend on where HeroService comes from. If you decided to provide HeroService in AppModule, HeroListComponent wouldn't change.

注入器樹與服務實例

Injector hierarchy and service instances

在某個注入器的範圍內,服務是單例的。也就是說,在指定的注入器中最多只有某個服務的最多一個實例。

Services are singletons within the scope of an injector. That is, there is at most one instance of a service in a given injector.

應用只有一個根注入器。在 rootAppModule 級提供 UserService 意味著它註冊到了根注入器上。 在整個應用中只有一個 UserService 實例,每個要求注入 UserService 的類別都會得到這一個服務實例,除非你在子注入器中配置了另一個提供者。

There is only one root injector for an app. Providing UserService at the root or AppModule level means it is registered with the root injector. There is just one UserService instance in the entire app and every class that injects UserService gets this service instance unless you configure another provider with a child injector.

Angular DI 具有分層注入體系,這意味著下級注入器也可以建立它們自己的服務實例。 Angular 會有規律的建立下級注入器。每當 Angular 建立一個在 @Component() 中指定了 providers 的元件實例時,它也會為該實例建立一個新的子注入器。 類似的,當在執行期間載入一個新的 NgModule 時,Angular 也可以為它建立一個擁有自己的提供者的注入器。

Angular DI has a hierarchical injection system, which means that nested injectors can create their own service instances. Angular regularly creates nested injectors. Whenever Angular creates a new instance of a component that has providers specified in @Component(), it also creates a new child injector for that instance. Similarly, when a new NgModule is lazy-loaded at run time, Angular can create an injector for it with its own providers.

子模組和元件注入器彼此獨立,並且會為所提供的服務分別建立自己的實例。當 Angular 銷燬 NgModule 或元件實例時,也會銷燬這些注入器以及注入器中的那些服務實例。

Child modules and component injectors are independent of each other, and create their own separate instances of the provided services. When Angular destroys an NgModule or component instance, it also destroys that injector and that injector's service instances.

藉助注入器繼承機制,你仍然可以把全應用級的服務注入到這些元件中。 元件的注入器是其父元件注入器的子節點,它會繼承所有的祖先注入器,其終點則是應用的注入器。 Angular 可以注入該繼承譜系中任何一個注入器提供的服務。

Thanks to injector inheritance, you can still inject application-wide services into these components. A component's injector is a child of its parent component's injector, and inherits from all ancestor injectors all the way back to the application's root injector. Angular can inject a service provided by any injector in that lineage.

比如,Angular 既可以把 HeroComponent 中提供的 HeroService 注入到 HeroListComponent,也可以注入 AppModule 中提供的 UserService

For example, Angular can inject HeroListComponent with both the HeroService provided in HeroComponent and the UserService provided in AppModule.

測試帶有依賴的元件

Testing components with dependencies

基於依賴注入設計一個類別,能讓它更易於測試。 要想高效的測試應用的各個部分,你所要做的一切就是把這些依賴列到建構函式的引數表中而已。

Designing a class with dependency injection makes the class easier to test. Listing dependencies as constructor parameters may be all you need to test application parts effectively.

比如,你可以使用一個可在測試期間操縱的模擬服務來建立新的 HeroListComponent

For example, you can create a new HeroListComponent with a mock service that you can manipulate under test.

const expectedHeroes = [{name: 'A'}, {name: 'B'}] const mockService = <HeroService> {getHeroes: () => expectedHeroes } it('should have heroes when HeroListComponent created', () => { // Pass the mock to the constructor as the Angular injector would const component = new HeroListComponent(mockService); expect(component.heroes.length).toEqual(expectedHeroes.length); });
src/app/test.component.ts
      
      const expectedHeroes = [{name: 'A'}, {name: 'B'}]
const mockService = <HeroService> {getHeroes: () => expectedHeroes }

it('should have heroes when HeroListComponent created', () => {
  // Pass the mock to the constructor as the Angular injector would
  const component = new HeroListComponent(mockService);
  expect(component.heroes.length).toEqual(expectedHeroes.length);
});
    

欲知詳情,參閱測試一章。

Learn more in the Testing guide.

那些需要其它服務的服務

Services that need other services

服務還可以具有自己的依賴。HeroService 非常簡單,沒有自己的依賴。不過,如果你希望透過日誌服務來報告這些活動,那麼就可以使用同樣的建構函式注入模式,新增一個建構函式來接收一個 Logger 引數。

Services can have their own dependencies. HeroService is very simple and doesn't have any dependencies of its own. Suppose, however, that you want it to report its activities through a logging service. You can apply the same constructor injection pattern, adding a constructor that takes a Logger parameter.

這是修改後的 HeroService,它注入了 Logger,我們把它和前一個版本的服務放在一起進行對比。

Here is the revised HeroService that injects Logger, side by side with the previous service for comparison.

import { Injectable } from '@angular/core'; import { HEROES } from './mock-heroes'; import { Logger } from '../logger.service'; @Injectable({ providedIn: 'root', }) export class HeroService { constructor(private logger: Logger) { } getHeroes() { this.logger.log('Getting heroes ...'); return HEROES; } }import { Injectable } from '@angular/core'; import { HEROES } from './mock-heroes'; @Injectable({ providedIn: 'root', }) export class HeroService { getHeroes() { return HEROES; } }import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class Logger { logs: string[] = []; // capture logs for testing log(message: string) { this.logs.push(message); console.log(message); } }
      
      import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';

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

  constructor(private logger: Logger) {  }

  getHeroes() {
    this.logger.log('Getting heroes ...');
    return HEROES;
  }
}
    

該建構函式請求注入一個 Logger 的實例,並把它儲存在一個名叫 logger 的私有欄位中。 當要求獲取英雄列表時,getHeroes() 方法就會記錄一條訊息。

The constructor asks for an injected instance of Logger and stores it in a private field called logger. The getHeroes() method logs a message when asked to fetch heroes.

注意,雖然 Logger 服務沒有自己的依賴項,但是它同樣帶有 @Injectable() 裝飾器。實際上,@Injectable() 對所有服務都是必須的

Notice that the Logger service also has the @Injectable() decorator, even though it might not need its own dependencies. In fact, the @Injectable() decorator is required for all services.

當 Angular 建立一個建構函式中有引數的類別時,它會查詢關於這些引數的型別,和供注入使用的元資料,以便找到正確的服務。 如果 Angular 無法找到引數資訊,它就會丟擲一個錯誤。 只有當類別具有某種裝飾器時,Angular 才能找到引數資訊。 @Injectable() 裝飾器是所有服務類別的標準裝飾器。

When Angular creates a class whose constructor has parameters, it looks for type and injection metadata about those parameters so that it can inject the correct service. If Angular can't find that parameter information, it throws an error. Angular can only find the parameter information if the class has a decorator of some kind. The @Injectable() decorator is the standard decorator for service classes.

裝飾器是 TypeScript 強制要求的。當 TypeScript 把程式碼轉譯成 JavaScript 時,一般會丟棄引數的型別資訊。只有當類別具有裝飾器,並且 tsconfig.json 中的編譯器選項 emitDecoratorMetadatatrue 時,TypeScript 才會保留這些資訊。CLI 所配置的 tsconfig.json 就帶有 emitDecoratorMetadata: true

The decorator requirement is imposed by TypeScript. TypeScript normally discards parameter type information when it transpiles the code to JavaScript. TypeScript preserves this information if the class has a decorator and the emitDecoratorMetadata compiler option is set true in TypeScript's tsconfig.json configuration file. The CLI configures tsconfig.json with emitDecoratorMetadata: true.

這意味著你有責任給所有服務類別加上 @Injectable()

This means you're responsible for putting @Injectable() on your service classes.

依賴注入令牌

Dependency injection tokens

當使用提供者配置注入器時,就會把提供者和一個 DI 令牌關聯起來。 注入器維護一個內部令牌-提供者的對映表,當請求一個依賴項時就會參考它。令牌就是這個對映表的鍵。

When you configure an injector with a provider, you associate that provider with a DI token. The injector maintains an internal token-provider map that it references when asked for a dependency. The token is the key to the map.

在簡單的例子中,依賴項的值是一個實例,而類別的型別則充當鍵來查閱它。 透過把 HeroService 型別作為令牌,你可以直接從注入器中獲得一個 HeroService 實例。

In simple examples, the dependency value is an instance, and the class type serves as its own lookup key. Here you get a HeroService directly from the injector by supplying the HeroService type as the token:

heroService: HeroService;
src/app/injector.component.ts
      
      heroService: HeroService;
    

當你編寫的建構函式中需要注入基於類別的依賴項時,其行為也類似。 當你使用 HeroService 類別的型別來定義建構函式引數時,Angular 就會知道要注入與 HeroService 類別這個令牌相關的服務。

The behavior is similar when you write a constructor that requires an injected class-based dependency. When you define a constructor parameter with the HeroService class type, Angular knows to inject the service associated with that HeroService class token:

constructor(heroService: HeroService)
src/app/heroes/hero-list.component.ts
      
      constructor(heroService: HeroService)
    

很多依賴項的值都是透過類別來提供的,但不是全部。擴充套件的 provide 物件讓你可以把多種不同種類的提供者和 DI 令牌關聯起來。

Many dependency values are provided by classes, but not all. The expanded provide object lets you associate different kinds of providers with a DI token.

可選依賴

Optional dependencies

HeroService 需要一個記錄器,但是如果找不到它會怎麼樣?

HeroService requires a logger, but what if it could get by without one?

當元件或服務宣告某個依賴項時,該類別的建構函式會以引數的形式接收那個依賴項。 透過給這個引數加上 @Optional() 註解,你可以告訴 Angular,該依賴是可選的。

When a component or service declares a dependency, the class constructor takes that dependency as a parameter. You can tell Angular that the dependency is optional by annotating the constructor parameter with @Optional().

import { Optional } from '@angular/core';
      
      import { Optional } from '@angular/core';
    
constructor(@Optional() private logger?: Logger) { if (this.logger) { this.logger.log(someMessage); } }
      
      constructor(@Optional() private logger?: Logger) {
  if (this.logger) {
    this.logger.log(someMessage);
  }
}
    

當使用 @Optional() 時,你的程式碼必須能正確處理 null 值。如果你沒有在任何地方註冊過 logger 提供者,那麼注入器就會把 logger 的值設定為 null。

When using @Optional(), your code must be prepared for a null value. If you don't register a logger provider anywhere, the injector sets the value of logger to null.

@Inject()@Optional() 都是引數裝飾器。它們透過在需要依賴項的類別的建構函式上對引數進行註解,來改變 DI 框架提供依賴項的方式。

@Inject() and @Optional() are parameter decorators. They alter the way the DI framework provides a dependency, by annotating the dependency parameter on the constructor of the class that requires the dependency.

欲知詳情,參閱多級注入器

Learn more about parameter decorators in Hierarchical Dependency Injectors.

小結

Summary

本頁中你學到了 Angular 依賴注入的基礎知識。 你可以註冊多種提供者,並且知道了如何透過為建構函式新增引數來請求所注入的物件(比如服務)。

You learned the basics of Angular dependency injection in this page. You can register various kinds of providers, and you know how to ask for an injected object (such as a service) by adding a parameter to a constructor.

在以下頁面中可以深入瞭解 Angular DI 體系的能力及高階特性:

Dive deeper into the capabilities and advanced feature of the Angular DI system in the following pages: