深入理解Angular中的依賴注入
一、什么是依賴注入
控制反轉(zhuǎn)(IoC)
控制反轉(zhuǎn)的概念最早在2004年由Martin Fowler提出,是針對(duì)面向?qū)ο笤O(shè)計(jì)不斷復(fù)雜化而提出的一種設(shè)計(jì)原則,是利用面向?qū)ο缶幊谭▌t來(lái)降低應(yīng)用耦合的設(shè)計(jì)模式。
IoC強(qiáng)調(diào)的是對(duì)代碼引用的控制權(quán)由調(diào)用方轉(zhuǎn)移到了外部容器,在運(yùn)行是通過(guò)某種方式注入進(jìn)來(lái),實(shí)現(xiàn)了控制反轉(zhuǎn),這大大降低了程序之間的耦合度。依賴注入是最常用的一種實(shí)現(xiàn)IoC的方式,另一種是依賴查找。
依賴注入(Dependency Injection)
當(dāng)然,按照慣例我們應(yīng)該舉個(gè)例子, 哦對(duì),我們主要說(shuō)明的是依賴注入,依賴查找請(qǐng)自行查閱資料。
假設(shè)我們有一個(gè)能做漢堡的設(shè)備(HRobot),需要用肉(meat)和一些沙拉(salad)作為原料,我們可以這樣實(shí)現(xiàn):
export class HRobot {
public meat: Meat;
public salad: Salad;
constructor() {
this.meat = new Meat();
this.salad = new Salad();
}
cook() {}
}
看一下好像沒(méi)有什么問(wèn)題,可能你已經(jīng)發(fā)現(xiàn),我們的原材料都是放在機(jī)器里面的,如果我們想吃別的口味的漢堡恐怕就要去鄉(xiāng)村基了。
為了可以吃到別的口味的漢堡,我們不得不改造一下我們的HRobot:
export class HRobot {
public meat: Meat;
public salad: Salad;
constructor(public meat: Meat, public salad: Salad) {
this.meat = meat;
this.salad = salad;
}
cook() {}
}
現(xiàn)在,只要要直接給它meat和salad就好了,我們的HRobot()并不需要知道給它的是什么樣的meat:
let hRobot = new HRobot(new Meat(), new Salad());
比如,我們想吃雞肉漢堡,只需要個(gè)它一塊雞肉就好:
class Chicken extends Meat {
meat = 'chiken';
}
let cRobot = new HRobot(new Chicken(), new Salad());
感覺(jué)還不錯(cuò),我們?cè)僖膊粫?huì)為了吃一個(gè)雞肉漢堡大費(fèi)周章的去改造一臺(tái)機(jī)器,這太不可思議了。
我可能想到了,你還是懶得弄塊雞肉給它,這時(shí)候可以使用工廠函數(shù):
export class HRobotFactory {
createHRobot() {
let robot = new HRobot(this.createMeat(), this.createSalad());
}
createMeat() {
return new Meat();
}
creatSalad() {
return new Salad();
}
}
現(xiàn)在有了工廠,就有源源不斷的漢堡可以吃了,開(kāi)不開(kāi)心,驚不驚喜?
好吧,沒(méi)有最懶,只有更懶,連工廠都懶得管理我也是無(wú)話可說(shuō),幸運(yùn)的是我們有Angular提供的依賴注入框架,它可以讓你伸手就有漢堡吃!
二、 Angular依賴注入
在介紹Angular依賴注入之前,先來(lái)理一下三個(gè)概念:
- 注入器(Injector):就想制造工廠,提供了一系列的接口,用于創(chuàng)建依賴對(duì)象的實(shí)例。
- 提供商(Provider):用于配置注入器,注入器通過(guò)它來(lái)創(chuàng)建被依賴對(duì)象的實(shí)例,Provider把令牌(Token)映射到工廠方法,被依賴的對(duì)象就是通過(guò)這個(gè)方法創(chuàng)建的。
- 依賴(Denpendence):指定了被依賴對(duì)象的類型,注入器會(huì)根據(jù)此類型創(chuàng)建對(duì)應(yīng)的對(duì)象。
說(shuō)了半天到底是什么樣的?

用代碼示例如下:
var injector = new Injector(...); var robot = injector.get(HRobot); robot.cook();
Injector()的實(shí)現(xiàn)如下:
import { ReflecttiveInjector } form '@angular/core';
var injector = ReflectiveInjector.resolveAndCreat([
{provide: HRobot, useClass: HRobot},
{provide: Meat, useClass: Meat},
{provide: Salad, useClass: Salad}
]);
還有注入器是這樣知道知道初始化HRobot需要依賴Meat和Salad:
export class Robot {
//...
consructor(public meat: Meat, public salad: Salad) {}
//...
}
當(dāng)然,看了頭大是應(yīng)該的,因?yàn)樯厦娴臇|西壓根就不需要自己動(dòng)手寫,Angular的依賴注入框架已經(jīng)自動(dòng)幫我們完成了(注入器的生成和調(diào)用)。
1. 在組件中注入服務(wù)
Angular在底層做了大量的初始化工作,這極大地降低了我們使用依賴注入的成本,現(xiàn)在要完成依賴注入,我們只需要三步:
- 通過(guò)import導(dǎo)入被依賴的對(duì)象服務(wù)
- 在組件中配置注入器。在啟動(dòng)組件時(shí),Angular會(huì)讀取@Component裝飾器里的providers元數(shù)據(jù),它是一個(gè)數(shù)組,配置了該組件需要使用的所有依賴,Angular的依賴注入框架會(huì)根據(jù)這個(gè)列表去創(chuàng)建對(duì)應(yīng)的示例。
- 在組件構(gòu)造函數(shù)中聲明需要注入的依賴。注入器會(huì)根據(jù)構(gòu)造函數(shù)上的聲明,在組件初始化時(shí)通過(guò)第二步中的providers元數(shù)據(jù)配置依賴,為構(gòu)造函數(shù)提供對(duì)應(yīng)的依賴服務(wù),最終完成依賴注入。
例子來(lái)了:
// app.component.ts
//...
// 1. 導(dǎo)入被依賴對(duì)象的服務(wù)
import { MyService } from './my-service/my-service.service';
@Component({
//...
// 2. 在組件中配置注入器
providers: [
MyService
]
//...
})
export class AppComponent {
// 3. 在構(gòu)造函數(shù)中聲明需要注入的依賴
constructor(private myService: MyService) {}
}
2. 在服務(wù)中注入服務(wù)
除了組件依賴服務(wù),服務(wù)間依的相互調(diào)用也很寒常見(jiàn)。例如我們想給我們的漢堡機(jī)器人加上一個(gè)計(jì)數(shù)器,來(lái)記錄它的生產(chǎn)狀況,但是計(jì)數(shù)器又依靠電源來(lái)工作,我們就可以用一個(gè)服務(wù)來(lái)實(shí)現(xiàn):
// power.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class PowerService {
// power come from here..
}
// count.service.ts
import { Injectable } from '@angular/core';
import { PowerService } from './power/power.service';
@Injectable()
export class CountService {
constructor(private power: PoowerService) {}
}
// app.component.ts 這里是當(dāng)前組件,其實(shí)模塊中的注入也一樣,后面講到
//...
providers: [
CountService,
PowerService
]
這里需要注意的是@Injectable裝飾器是非必須的,因?yàn)橹挥幸粋€(gè)服務(wù)依賴其他服務(wù)的時(shí)候才必須需要使用@Injectable顯式裝飾,來(lái)表示這個(gè)服務(wù)需要依賴,所以我們的PowerService并不是必須加上@Injectable裝飾器的,可是,Angular官方推薦是否依賴其他服務(wù),都應(yīng)該使用@Injectable來(lái)裝飾服務(wù)。
3. 在模塊中注入服務(wù)
在模塊中注冊(cè)服務(wù)和在組件中注冊(cè)服務(wù)的方法是一樣的,只是在模塊中注入的服務(wù)在整個(gè)組件中都是可用的。
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule
],
providers: [CountService, PowerService],
bootstrap: [AppComponent]
})
export class AppModule { }
與在組件中注入不同的是,在Angular應(yīng)用啟動(dòng)的時(shí)候,它好首先加載這個(gè)模塊需要的所有依賴,,此時(shí)會(huì)生成一個(gè)全局的根注入器,由該依賴創(chuàng)建的依賴注入對(duì)象會(huì)再整個(gè)應(yīng)用中可見(jiàn),并共享一個(gè)實(shí)例。
Angular沒(méi)有模塊級(jí)作用域這個(gè)概念,只有應(yīng)用程序級(jí)作用域和組件級(jí)作用域,這種設(shè)計(jì)主要是考慮模塊的擴(kuò)展性,一個(gè)應(yīng)用通常由多個(gè)模塊合并和成,在@NgModule中注冊(cè)的服務(wù),默認(rèn)在整個(gè)應(yīng)用中可用。
下面說(shuō)兩種特殊情況:
假設(shè)在兩個(gè)模塊中使用同樣的Token注入了同一個(gè)服務(wù),并且這兩個(gè)模塊先后導(dǎo)入到了根組件中:
// ...
@NgModule({
imports: [
AModule,
BModule
]
// ...
})
那么后面導(dǎo)入的模塊中的服務(wù)會(huì)覆蓋前面導(dǎo)入模塊中的服務(wù),也就是說(shuō)BModule中的服務(wù)會(huì)覆蓋AModule中的服務(wù),即使是在AModule中注入的服務(wù),同樣使用的是BMoudle中提供的實(shí)例。
還是假設(shè)兩個(gè)模塊同樣使用同一個(gè)Token注入了同一個(gè)服務(wù),但是BModule模塊是導(dǎo)入在AModule模塊中的:
// a.module.ts
// ...
@NgModule({
imports: [BModule]
})
那么這種情況下兩個(gè)模塊使用的都是AModule中注入的服務(wù)。可以推斷出在根模塊中注入的服務(wù)是擁有最高優(yōu)先級(jí)的,你可以在任何地方放心使用。
三、Provider
1. Provider的理解
Provider是有必要單獨(dú)提出來(lái)一節(jié)的,上面第二節(jié)中我們其實(shí)只是簡(jiǎn)單的使用了其中一種的provider下面來(lái)詳細(xì)說(shuō)一下Provider
在Angular中,Provider描述了注入器(Injector)如何初始化令牌(Token)所對(duì)應(yīng)的依賴服務(wù)。Provider一個(gè)運(yùn)行時(shí)的依賴,注入器依靠它來(lái)創(chuàng)建服務(wù)對(duì)象的實(shí)例。
比如我們上面用到的例子:
// ...
@Component({
//...
// 2. 在組件中配置注入器
providers: [
MyService
]
//...
})
實(shí)際上它的完整形式應(yīng)該是這樣的:
@Component({
//...
// 2. 在組件中配置注入器
providers: [
{provide: MyService, useClass: MyService}
]
//...
})
所以說(shuō)我們上面只使用了一種provider: 類Provider(ClassProvider)。
2. Provider注冊(cè)方式
上面提到我只使用了其中一種注冊(cè)方式,那么下面介紹Angular中提供的四中常見(jiàn)的注冊(cè)方式:
- 類Provider(ClassProvider)
- 值Provider(ValueProvider)
- 別名Provider(ExistingProvider)
- 工廠Provider(FactoryProvider)
1. 類Provider
類Provider 基于令牌(Token)指定依賴項(xiàng),這種方式可是讓依賴被動(dòng)態(tài)指定為其他不同的具體實(shí)現(xiàn),只要接口不變,對(duì)于使用方就是透明的。比如數(shù)據(jù)渲染服務(wù)(Render),Render服務(wù)對(duì)上層提供的接口是固定的,倒是底層的渲染方式可以不同:
```ts
var inject = Injector.resolveAndCreate([
{provide: Render, useClass: DomRender}
//{provide: Render, useClass: DomRender} // canvas 渲染方式
//{provide: Render, useClass: DomRender} // 服務(wù)的想染方式
])
// 調(diào)用方不用做任何修改
class AppComponent {
construtor(private render: Render) {}
}
```
2. 值Provider
由于依賴的對(duì)象并不一定都是類,也可以是字符串、常量、對(duì)象等其他數(shù)據(jù)類型的,這可以方便用在全局變量、系統(tǒng)相關(guān)參數(shù)配置場(chǎng)景中。在創(chuàng)建Provider對(duì)象的時(shí)候,只需要使用useValue就可以聲明一個(gè)值Provider:
```ts
let freeMan = {
freeJob: boolen;
live: () => {return 'do something u cant do'}
};
@Component({
// ...
providers: [
{provide: 'someone', useValue: freeMan}
]
})
```
3. 別名Provider
有了別名Provider,我們就可以在一個(gè)Provider中配置多個(gè)令牌(Token),其對(duì)于的對(duì)象指向同一個(gè)實(shí)例,從而實(shí)現(xiàn)了多個(gè)依賴、一個(gè)對(duì)象實(shí)例的作用:
// ...
providers: [
{provider: Power1, useClass: PowerService},
{provider: Power2, useClass: PowerService}
]
// ...
仔細(xì)想想,這樣對(duì)嗎?
顯然是不對(duì)的,如果兩個(gè)都使用了useClass那么按照令牌,將會(huì)創(chuàng)建兩個(gè)不同的實(shí)例出來(lái),那么應(yīng)該怎么實(shí)現(xiàn)兩個(gè)令牌同一個(gè)實(shí)例呢?答案是使用useExistiong:
// ...
providers: [
{provider: Power1, useClass: PowerService},
{provider: Power2, useExisting: PowerService}
]
// ...
4. 工廠Provider
工廠Provider允許我們根據(jù)不同的條件來(lái)實(shí)例化不同的服務(wù),比如,我們?cè)陂_(kāi)發(fā)環(huán)境需要打印日志,但是在實(shí)際部署的時(shí)候可能并不需要打印這些東西,那么我們總不可能去找到整個(gè)應(yīng)用中所有的console.log()這樣的方法吧,這個(gè)時(shí)候我們可以使用工廠provider來(lái)幫我們處理,我們只需要在工廠provider中設(shè)定一個(gè)條件,使其能夠根據(jù)條件返回實(shí)例化我們需要的服務(wù)就可以了。為了實(shí)現(xiàn)這樣的功能我們可以在根模塊中這樣注入:
```ts
// app.module.ts
@NgModule({
// ...
providers: [
HeroService,
ConsoleService,
{
provide: LoggerService,
useFactory: (consoleService) => {
return new LoggerService(true, consoleService);
},
deps: [ConsoleService]
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
哦哦,那兩個(gè)服務(wù)是這樣寫的:
// console.service.ts
// ...
export class ConsoleService {
log(message) {
console.log(`ConsoleService: ${message}`);
}
}
// logger.service.ts
// ...
export class LoggerService {
constructor(private enable: boolean,
consoleService: ConsoleService
) { }
log(message: string) {
if (this.enable) {
console.log(`LoggerService: ${message}`);
}
}
}
然后在組件構(gòu)造函數(shù)中寫上需要的服務(wù)就好。
四、限定方式的依賴注入
想象一場(chǎng)景,你應(yīng)用中的某個(gè)服務(wù)的provider被當(dāng)做無(wú)效代碼刪掉了,那么你的應(yīng)用可能就會(huì)出問(wèn)題。還好這個(gè)問(wèn)題早在設(shè)計(jì)的時(shí)候就已經(jīng)考慮到了,我們可以使用Angular提供的@Optional和@Host裝飾器來(lái)解決這個(gè)問(wèn)題。
Optional可以兼容依賴不存在的情況,提高系統(tǒng)的健壯性;@Host可以限定查找規(guī)則,明確實(shí)例化的位置,避免一些莫名的共享對(duì)象問(wèn)題。
@Optional
借助@Optional就可以實(shí)現(xiàn)可選注入:
// app.component.ts
// ...
import { Optional } from '@angular/core';
constructor(@Optional() private logger: LoggerService) {
if (this.logger) {
this.logger.log('i am choosed');
}
}
像例子中的那樣只需要在宿主組件(Host Component)的構(gòu)造函數(shù)中增加@Optional裝飾器即可。
需要注意的是,上面例子中的LoggerService并不是不存在,只是并沒(méi)有根據(jù)providers元數(shù)據(jù)中配置被實(shí)例化出來(lái)。
@Host
Angular中依賴查找的規(guī)則是按照注入器從當(dāng)前組件向父組件查找,直到找到要注入的依賴位置,如果找不到就會(huì)報(bào)錯(cuò)。我們可以使用Angular提供的@Host裝飾器來(lái)解決 這個(gè)問(wèn)題。
宿主組件如果一個(gè)組件注入了依賴項(xiàng),那么這個(gè)組件就是這個(gè)依賴的宿主組件;如果這個(gè)組件通過(guò)<ng-content>被嵌入到了父組件,那這個(gè)父組件就是這個(gè)依賴的宿主組件。
1、宿主組件是當(dāng)前組件
我們給組件構(gòu)造函數(shù)加上@Host裝飾器:
// ...
@Component({
selector: 'parent',
template: `
<h1>這里是父組件</h1>
`
})
constructor(
@Host()
logger: LoggerService) {}
// 加上@Host之后會(huì)報(bào)錯(cuò),因?yàn)槲覀儾](méi)有在這個(gè)組件中注入LoggerService
// 但是我們可以加上@Optional來(lái)避免報(bào)錯(cuò)
//@Host()
//@Optional()
//logger: LoggerService) {}
)
2、宿主組件是父組件
我們修改一下上面的組件為父組件:
// parent.component.ts
// ...
@Component({
selector: 'parent',
template: `
<h1>這里是父組件</h1>
<ng-content></content>
`
// 在父組件中注入 LoggerService
providers: [LoggerService]
})
constructor() {}
增加一個(gè)子組件:
// child.component.ts
// ...
@Component({
selector: 'child',
template: `
<h1>這里是子組件</h1>
`
})
constructor(
@Host()
@Optional()
logger: LoggerService)
){}
當(dāng)然<parent>標(biāo)簽中應(yīng)該這樣寫:
<parent> <child></child> </parent>
因?yàn)榇藭r(shí)宿主組件是父組件,所以我們?cè)诟附M件中注入LoggerService Angular注入器會(huì)自動(dòng)向上查找,找到ParentComponet中的配置,從而完成注入。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
AngularJS 將再發(fā)布一個(gè)重要版本 然后進(jìn)入長(zhǎng)期支持階段
目前團(tuán)隊(duì)正在開(kāi)發(fā) AngularJS 1.7.0,而 1.7 的開(kāi)發(fā)周期將一直持續(xù)到 2018 年 6 月 30 日2018-01-01
淺談AngularJs 雙向綁定原理(數(shù)據(jù)綁定機(jī)制)
本篇文章主要介紹了淺談AngularJs 雙向綁定原理(數(shù)據(jù)綁定機(jī)制),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
AngularJS使用ocLazyLoad實(shí)現(xiàn)js延遲加載
這篇文章主要介紹了AngularJS使用ocLazyLoad實(shí)現(xiàn)js延遲加載的相關(guān)資料,需要的朋友可以參考下2017-07-07
Angular6 Filter實(shí)現(xiàn)頁(yè)面搜索的示例代碼
這篇文章主要介紹了Angular6 Filter實(shí)現(xiàn)頁(yè)面搜索的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
Angular2+國(guó)際化方案(ngx-translate)的示例代碼
本篇文章主要介紹了Angular2+國(guó)際化方案(ngx-translate)的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
angularjs http與后臺(tái)交互的實(shí)現(xiàn)示例
這篇文章主要介紹了angularjs http與后臺(tái)交互的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12

