依賴注入(DI
)是一種重要的應(yīng)用設(shè)計(jì)模式。 Angular 有自己的 DI
框架,在設(shè)計(jì)應(yīng)用時(shí)常會(huì)用到它,以提升它們的開發(fā)效率和模塊化程度。
依賴,是當(dāng)類需要執(zhí)行其功能時(shí),所需要的服務(wù)或?qū)ο蟆?DI
是一種編碼模式,其中的類會(huì)從外部源中請(qǐng)求獲取依賴,而不是自己創(chuàng)建它們。
在 Angular 中,DI
框架會(huì)在實(shí)例化該類時(shí)向其提供這個(gè)類所聲明的依賴項(xiàng)。本指南介紹了 DI
在 Angular 中的工作原理,以及如何借助它來(lái)讓你的應(yīng)用更靈活、高效、健壯,以及可測(cè)試、可維護(hù)。
我們先看一下英雄指南中英雄管理特性的簡(jiǎn)化版。這個(gè)簡(jiǎn)化版不使用 DI
,我們將逐步把它轉(zhuǎn)換成使用 DI
的。
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' }
];
HeroesComponent
是頂層英雄管理組件。 它唯一的目的是顯示 HeroListComponent
,該組件會(huì)顯示一個(gè)英雄名字的列表。
HeroListComponent
的這個(gè)版本從 HEROES 數(shù)組(它在一個(gè)獨(dú)立的 "mock-heroes" 文件中定義了一個(gè)內(nèi)存集合)中獲取英雄。
Path:"src/app/heroes/hero-list.component.ts (class)" 。
export class HeroListComponent {
heroes = HEROES;
}
這種方法在原型階段有用,但是不夠健壯、不利于維護(hù)。 一旦你想要測(cè)試該組件或想從遠(yuǎn)程服務(wù)器獲得英雄列表,就不得不修改 HeroesListComponent
的實(shí)現(xiàn),并且替換每一處使用了 HEROES
模擬數(shù)據(jù)的地方。
DI
框架讓你能從一個(gè)可注入的服務(wù)類(獨(dú)立文件)中為組件提供數(shù)據(jù)。為了演示,我們還會(huì)創(chuàng)建一個(gè)用來(lái)提供英雄列表的、可注入的服務(wù)類,并把它注冊(cè)為該服務(wù)的提供者。
同一個(gè)文件中放多個(gè)類容易讓人困惑。我們通常建議你在單獨(dú)的文件中定義組件和服務(wù)。
如果你把組件和服務(wù)都放在同一個(gè)文件中,請(qǐng)務(wù)必先定義服務(wù),然后再定義組件。如果在服務(wù)之前定義組件,則會(huì)在運(yùn)行時(shí)收到一個(gè)空引用錯(cuò)誤。
也可以借助
forwardRef()
方法來(lái)先定義組件,就像這個(gè)博客中解釋的那樣。
Angular CLI 可以用下列命令在 "src/app/heroes" 目錄下生成一個(gè)新的 HeroService
類。
ng generate service heroes/hero
下列命令會(huì)創(chuàng)建 HeroService
的骨架。
Path:"src/app/heroes/hero.service.ts (CLI-generated)" 。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}
@Injectable()
是每個(gè) Angular 服務(wù)定義中的基本要素。該類的其余部分導(dǎo)出了一個(gè) getHeroes
方法,它會(huì)返回像以前一樣的模擬數(shù)據(jù)。(真實(shí)的應(yīng)用可能會(huì)從遠(yuǎn)程服務(wù)器中異步獲取這些數(shù)據(jù),不過(guò)這里我們先忽略它,專心實(shí)現(xiàn)服務(wù)的注入機(jī)制。)
Path:"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; }
}
我們創(chuàng)建的類提供了一個(gè)服務(wù)。@Injectable()
裝飾器把它標(biāo)記為可供注入的服務(wù),不過(guò)在你使用該服務(wù)的 provider
提供者配置好 Angular 的依賴注入器之前,Angular 實(shí)際上無(wú)法將其注入到任何位置。
該注入器負(fù)責(zé)創(chuàng)建服務(wù)實(shí)例,并把它們注入到像 HeroListComponent
這樣的類中。 你很少需要自己創(chuàng)建 Angular 的注入器。Angular 會(huì)在執(zhí)行應(yīng)用時(shí)為你創(chuàng)建注入器,第一個(gè)注入器是根注入器,創(chuàng)建于啟動(dòng)過(guò)程中。
提供者會(huì)告訴注入器如何創(chuàng)建該服務(wù)。 要想讓注入器能夠創(chuàng)建服務(wù)(或提供其它類型的依賴),你必須使用某個(gè)提供者配置好注入器。
提供者可以是服務(wù)類本身,因此注入器可以使用 new
來(lái)創(chuàng)建實(shí)例。 你還可以定義多個(gè)類,以不同的方式提供同一個(gè)服務(wù),并使用不同的提供者來(lái)配置不同的注入器。
注入器是可繼承的,這意味著如果指定的注入器無(wú)法解析某個(gè)依賴,它就會(huì)請(qǐng)求父注入器來(lái)解析它。 組件可以從它自己的注入器來(lái)獲取服務(wù)、從其祖先組件的注入器中獲取、從其父
NgModule
的注入器中獲取,或從root
注入器中獲取。
你可以在三種位置之一設(shè)置元數(shù)據(jù),以便在應(yīng)用的不同層級(jí)使用提供者來(lái)配置注入器:
@Injectable()
裝飾器中。NgModule
的 @NgModule()
裝飾器中。@Component()
裝飾器中。
@Injectable()
裝飾器具有一個(gè)名叫 providedIn
的元數(shù)據(jù)選項(xiàng),在那里你可以指定把被裝飾類的提供者放到 root
注入器中,或某個(gè)特定 NgModule
的注入器中。
@NgModule()
和 @Component()
裝飾器都有用一個(gè) providers
元數(shù)據(jù)選項(xiàng),在那里你可以配置 NgModule
級(jí)或組件級(jí)的注入器。
所有組件都是指令,而
providers
選項(xiàng)是從@Directive()
中繼承來(lái)的。 你也可以與組件一樣的級(jí)別為指令、管道配置提供者。
HeroListComponent
要想從 HeroService
中獲取英雄列表,就得要求注入 HeroService
,而不是自己使用 new
來(lái)創(chuàng)建自己的 HeroService
實(shí)例。
你可以通過(guò)制定帶有依賴類型的構(gòu)造函數(shù)參數(shù)來(lái)要求 Angular 在組件的構(gòu)造函數(shù)中注入依賴項(xiàng)。下面的代碼是 HeroListComponent
的構(gòu)造函數(shù),它要求注入 HeroService
。
Path:"src/app/heroes/hero-list.component (constructor signature)" 。
constructor(heroService: HeroService)
當(dāng)然,HeroListComponent
還應(yīng)該使用注入的這個(gè) HeroService
做一些事情。 這里是修改過(guò)的組件,它轉(zhuǎn)而使用注入的服務(wù)。與前一版本并列顯示,以便比較。
//hero-list.component (with DI)
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();
}
}
//hero-list.component (without DI)
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;
}
必須在某些父注入器中提供 HeroService
。HeroListComponent
并不關(guān)心 HeroService
來(lái)自哪里。 如果你決定在 AppModule
中提供 HeroService
,也不必修改 HeroListComponent
。
在某個(gè)注入器的范圍內(nèi),服務(wù)是單例的。也就是說(shuō),在指定的注入器中最多只有某個(gè)服務(wù)的最多一個(gè)實(shí)例。
應(yīng)用只有一個(gè)根注入器。在 root
或 AppModule
級(jí)提供 UserService
意味著它注冊(cè)到了根注入器上。 在整個(gè)應(yīng)用中只有一個(gè) UserService
實(shí)例,每個(gè)要求注入 UserService
的類都會(huì)得到這一個(gè)服務(wù)實(shí)例,除非你在子注入器中配置了另一個(gè)提供者。
Angular DI 具有分層注入體系,這意味著下級(jí)注入器也可以創(chuàng)建它們自己的服務(wù)實(shí)例。 Angular 會(huì)有規(guī)律的創(chuàng)建下級(jí)注入器。每當(dāng) Angular 創(chuàng)建一個(gè)在 @Component()
中指定了 providers
的組件實(shí)例時(shí),它也會(huì)為該實(shí)例創(chuàng)建一個(gè)新的子注入器。 類似的,當(dāng)在運(yùn)行期間加載一個(gè)新的 NgModule
時(shí),Angular 也可以為它創(chuàng)建一個(gè)擁有自己的提供者的注入器。
子模塊和組件注入器彼此獨(dú)立,并且會(huì)為所提供的服務(wù)分別創(chuàng)建自己的實(shí)例。當(dāng) Angular 銷毀 NgModule
或組件實(shí)例時(shí),也會(huì)銷毀這些注入器以及注入器中的那些服務(wù)實(shí)例。
借助注入器繼承機(jī)制,你仍然可以把全應(yīng)用級(jí)的服務(wù)注入到這些組件中。 組件的注入器是其父組件注入器的子節(jié)點(diǎn),它會(huì)繼承所有的祖先注入器,其終點(diǎn)則是應(yīng)用的根注入器。 Angular 可以注入該繼承譜系中任何一個(gè)注入器提供的服務(wù)。
比如,Angular 既可以把 HeroComponent
中提供的 HeroService
注入到 HeroListComponent
,也可以注入 AppModule
中提供的 UserService
。
基于依賴注入設(shè)計(jì)一個(gè)類,能讓它更易于測(cè)試。 要想高效的測(cè)試應(yīng)用的各個(gè)部分,你所要做的一切就是把這些依賴列到構(gòu)造函數(shù)的參數(shù)表中而已。
比如,你可以使用一個(gè)可在測(cè)試期間操縱的模擬服務(wù)來(lái)創(chuàng)建新的 HeroListComponent
。
Path:"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);
});
服務(wù)還可以具有自己的依賴。HeroService
非常簡(jiǎn)單,沒(méi)有自己的依賴。不過(guò),如果你希望通過(guò)日志服務(wù)來(lái)報(bào)告這些活動(dòng),那么就可以使用同樣的構(gòu)造函數(shù)注入模式,添加一個(gè)構(gòu)造函數(shù)來(lái)接收一個(gè) Logger
參數(shù)。
這是修改后的 HeroService
,它注入了 Logger
,我們把它和前一個(gè)版本的服務(wù)放在一起進(jìn)行對(duì)比。
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);
}
}
該構(gòu)造函數(shù)請(qǐng)求注入一個(gè) Logger
的實(shí)例,并把它保存在一個(gè)名叫 logger
的私有字段中。 當(dāng)要求獲取英雄列表時(shí),getHeroes()
方法就會(huì)記錄一條消息。
注意,雖然 Logger
服務(wù)沒(méi)有自己的依賴項(xiàng),但是它同樣帶有 @Injectable()
裝飾器。實(shí)際上,@Injectable()
對(duì)所有服務(wù)都是必須的。
當(dāng) Angular 創(chuàng)建一個(gè)構(gòu)造函數(shù)中有參數(shù)的類時(shí),它會(huì)查找有關(guān)這些參數(shù)的類型,和供注入使用的元數(shù)據(jù),以便找到正確的服務(wù)。 如果 Angular 無(wú)法找到參數(shù)信息,它就會(huì)拋出一個(gè)錯(cuò)誤。 只有當(dāng)類具有某種裝飾器時(shí),Angular 才能找到參數(shù)信息。 @Injectable()
裝飾器是所有服務(wù)類的標(biāo)準(zhǔn)裝飾器。
裝飾器是 TypeScript 強(qiáng)制要求的。當(dāng) TypeScript 把代碼轉(zhuǎn)譯成 JavaScript 時(shí),一般會(huì)丟棄參數(shù)的類型信息。只有當(dāng)類具有裝飾器,并且 "tsconfig.json" 中的編譯器選項(xiàng)
emitDecoratorMetadata
為true
時(shí),TypeScript 才會(huì)保留這些信息。CLI 所配置的 "tsconfig.json" 就帶有emitDecoratorMetadata: true
。
這意味著你有責(zé)任給所有服務(wù)類加上
@Injectable()
。
當(dāng)使用提供者配置注入器時(shí),就會(huì)把提供者和一個(gè) DI
令牌關(guān)聯(lián)起來(lái)。 注入器維護(hù)一個(gè)內(nèi)部令牌-提供者的映射表,當(dāng)請(qǐng)求一個(gè)依賴項(xiàng)時(shí)就會(huì)引用它。令牌就是這個(gè)映射表的鍵。
在簡(jiǎn)單的例子中,依賴項(xiàng)的值是一個(gè)實(shí)例,而類的類型則充當(dāng)鍵來(lái)查閱它。 通過(guò)把 HeroService
類型作為令牌,你可以直接從注入器中獲得一個(gè) HeroService
實(shí)例。
Path:"src/app/injector.component.ts" 。
heroService: HeroService;
當(dāng)你編寫的構(gòu)造函數(shù)中需要注入基于類的依賴項(xiàng)時(shí),其行為也類似。 當(dāng)你使用 HeroService
類的類型來(lái)定義構(gòu)造函數(shù)參數(shù)時(shí),Angular 就會(huì)知道要注入與 HeroService
類這個(gè)令牌相關(guān)的服務(wù)。
Path:"src/app/heroes/hero-list.component.ts" 。
constructor(heroService: HeroService)
很多依賴項(xiàng)的值都是通過(guò)類來(lái)提供的,但不是全部。擴(kuò)展的 provide
對(duì)象讓你可以把多種不同種類的提供者和 DI
令牌關(guān)聯(lián)起來(lái)。
HeroService
需要一個(gè)記錄器,但是如果找不到它會(huì)怎么樣?
當(dāng)組件或服務(wù)聲明某個(gè)依賴項(xiàng)時(shí),該類的構(gòu)造函數(shù)會(huì)以參數(shù)的形式接收那個(gè)依賴項(xiàng)。 通過(guò)給這個(gè)參數(shù)加上 @Optional()
注解,你可以告訴 Angular,該依賴是可選的。
import { Optional } from '@angular/core';
constructor(@Optional() private logger?: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}
當(dāng)使用 @Optional()
時(shí),你的代碼必須能正確處理 null
值。如果你沒(méi)有在任何地方注冊(cè)過(guò) logger
提供者,那么注入器就會(huì)把 logger
的值設(shè)置為 null
。
@Inject()
和@Optional()
都是參數(shù)裝飾器。它們通過(guò)在需要依賴項(xiàng)的類的構(gòu)造函數(shù)上對(duì)參數(shù)進(jìn)行注解,來(lái)改變 DI 框架提供依賴項(xiàng)的方式。
本節(jié)中你學(xué)到了 Angular 依賴注入的基礎(chǔ)知識(shí)。 你可以注冊(cè)多種提供者,并且知道了如何通過(guò)為構(gòu)造函數(shù)添加參數(shù)來(lái)請(qǐng)求所注入的對(duì)象(比如服務(wù))。
更多建議: