引言
依賴注入,有后端背景的童鞋(尤其是熟悉java spring框架的)應該不會陌生,提到依賴注入,就不得不說一下控制反轉,因為依賴注入實際上是控制反轉概念的一種實現模式。
控制反轉(inversion of control,IoC)原則的非正式稱謂是“好萊塢法則”,它來自好萊塢的一句常用語“別打給我們,我們會打給你(don't call us, we'll call you)”,從控制權的角度來看,控制權從我被反轉到了別人,我從打電話這個行為的發出者,變成了打電話這個行為的接受者。
控制“正”轉可以理解為面向過程編程,定義一個main函數,在main函數里面定義所有的參與者與他們之間的交互過程,一點不能偷懶。需要用到某個對象某個類,自己去new,需要某個庫函數,自己去主動調用。
控制“反”轉可以理解為面向對象編程,我們有了各種適用于不同場景下的框架和設計模式,這些事物幫我們搭建好了一個整體架構,這個架構用于準備一些基本而普遍需要的基礎物資。
控制反轉的設計目標
- 將執行任務這個動作和任務(具體實現)解耦
- 讓模塊專注于它自己的設計目標。不需要考慮別的模塊實現了什么,是如何實現的,模塊之間依賴于接口
- 讓符合相同契約的模塊能夠互相替代
遵循控制反轉的幾個事物
- 框架:有些太繁瑣太基礎的事情我已經幫你做好了,我做不了的那部分以坑的形式留給你了,填完坑你的工作也就完成了,后邊的事情我來處理
- 回調、調度器、事件循環:把坑填好,等到時機成熟我就來和你相會
策略模式:坑的游戲規則我已經定義好(輸入輸出),你只需要按規則填坑就好 - 模板方式模式:坑的種類和踩坑的順序我已定義好,你只需要負責填坑
- 工廠模式和依賴注入類似,下面詳細分析
依賴注入
在任何一個有請求作用的系統當中,至少需要有兩個類互相配合工作,在一個入口類下使用new關鍵字創建另一個類的對象實例,“我”充當一個入口類,在這個入口類中,我每次吃飯的時候都要買一雙一次性筷子(每一次使用都要new一次),在這樣的關系下,是“我”(即調用者)每次都要“主動”去買一次性筷子(另一個類),是我控制了筷子,在這種控制正轉的關系下,放在現實生活當中,肯定是不現實的,而且人是懶惰的,他總會去創造出更加方便自己生活的想法,更確切的做法是,買一雙普通的筷子(非一次性),把他放在一個“框架”當中,你需要使用的時候就對“框架”說:我想要用筷子(向框架發出請求),接著筷子就會"注入"到你的手上,而在這個過程當中,你不再是控制方,反而演變成一名請求者(雖然本身還是調用者),依賴于框架給予你資源,控制權落到了框架身上,這就是依賴注入的設計原理。
要理解依賴注入,就必須搞清楚幾個問題:
參與者:都有誰
依賴:誰依賴于誰,為什么需要依賴
注入:誰注入誰,到底注入什么
控制:誰控制誰,控制什么
參與者:
一般有三方參與者,參與者1:某個對象,參與者2:DI框架,參與者3:對象所需的外部資源(類,文件等等)
依賴:
對象依賴于DI框架
為什么需要依賴:
“對象(參與者1)”需要“DI框架(參與者2)”提供所需的“外部資源(參與者3)”
誰注入誰:
DI框架注入“對象(參與者1)”
注入什么:
注入“對象(參與者1)”所需的“外部資源(參與者3)”
誰控制誰:
“DI框架(參與者2)”控制對象(參與者1)”
控制什么:
主要是控制對象所需外部資源實例的創建,管理所需重要對象的生命周期
反轉:
在傳統方式下,也就是正向,如果部件A需要依賴部件B,那就意味著A在內部要創建一個B的實例,也就是A依賴于B。在依賴注入機制也就是反向,如果在部件A中用到部件B,我們就應該期待B被傳給A
依賴注入:它的作用是讓框架幫你處理重要對象的生命周期的管理,不需要你顯式地進行管理(對象構造和銷毀)
優勢:架構松耦合,測試更簡單
Angular 依賴注入
在angular中,依賴注入包括三部分
- 提供商:負責把一個令牌(可能是字符串也可能是類)映射到一個依賴的列表,它告訴angular該如何根據指定的令牌創建對象
- 注入器:負責持有一組綁定,當外界要求創建對象時,解析這些依賴并注入它們。我們不需要創建angular注入器,angular在啟動過程中會自動為我們創建一個應用級注入器(
platformBrowserDynamic().bootstrapModule(AppModule)
) - 依賴:被用于注入的對象
注入器的提供商
- 類提供商 useClass
providers: [Logger],這其實是注冊提供商的簡寫表達式,完整的應該是providers: [{provide: Logger,useClass: Logger}]
provide是令牌,用于定位依賴值和注冊提供商
useClass是提供商,用于定義對象,它用來指出注入什么以及如何注入
某些時候,我們會請求一個不同的類來提供服務:
providers: [{provide: Logger,useClass: BetterLogger}]
- 別名提供商 useExisting
制造一個別名來引用以前注冊過的令牌,比如:
某個舊組件依賴一個OldLogger類,OldLogger和NewLogger具有相同的接口,但是由于某些原因, 我們不能升級這個舊組件并使用它。當舊組件想使用OldLogger記錄消息時,我們希望改用NewLogger的單例對象來記錄,不管組件請求的是新的還是舊的日志服務,依賴注入器注入的都應該是同一個單例對象。 也就是說,OldLogger應該是NewLogger的別名。如果使用useClass應用中會有兩個不同的NewLogger實例,這顯然不是我們想看到的,這時候就可以使用useExisting
providers: [
NewLogger,
{provide: OldLogger,useClass: NewLogger} //創建NewLogger的兩個實例
]
NewLogger,
{provide: OldLogger,useExisting: NewLogger} //只創建NewLogger的一個實例
]
- 值提供商 useValue
當我們需要一個常量,而它可能會根據應用的其他部分甚至環境進行重定義時,這種方式非常重要。
官方推薦使用InjectionToken作為令牌
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken <string>('title');
providers:[
{provide:TITLE, useValue: 'Hero of the Month'}
]
- 工廠提供商 useFactory
使用工廠提供商進行注入,需要寫一個返回任意對象的函數,工廠是創建可諸如對象的最強方式,因為我們可以在工廠函數中“為所欲為”。
providers:[{
provide: MyComponent,
useFactory: ()=> {
if(loggedIn) {
return new MyloggedComponent();
}
return new MyComponent();
}
}]
依賴注入令牌
- 類
我們最常用的
providers: [{provide: Logger,useClass: BetterLogger}]
- 類-接口
使用沒有被繼承的抽象類作為依賴注入令牌,這種用法的類叫做:類-接口,它的好處是:提供了接口的強類型,能像正常類一樣把它當做提供商令牌使用。類-接口應該只定義允許它消費者調用的成員,窄的接口有助于解耦該類的具體實現和它的消費者。
不能使用接口作為依賴注入令牌,因為在JavaScript中并沒有接口的概念,編譯后angular無法識別
更詳細內容參考angular官方文檔 - InjectionToken
有時候依賴對象并不是一個類,它可能是一個簡單的值,比如日期,數字和字符串,或者一個無形的對象,比如數組和函數。
這樣的對象沒有應用程序接口,所以不能用一個類來表示,更適合表示它們的是:唯一的和符號性的令牌,一個JavaScript對象,擁有一個友好的名字,但不會與其它的同名令牌發生沖突。
官方推薦使用InjectionToken實現(其實直接使用字符串也可以,最好還是按照官方推薦方式O(∩_∩)O)
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken <string>('title');
export const RUNNERS_UP= new InjectionToken <string>('RunnersUP');
providers:[
{provide:TITLE, useValue: 'Hero of the Month'},
{RUNNERS_UP, useFactory: runnersUpFactory(2),dep[Hero, HeroService]}
]
注冊提供商
可以在NgModule或者組件中注冊提供商
通常推薦在對應的模塊(NgModule)中注冊提供商,除非你必須把服務實例的范圍限制到某個組件及其子組件樹
- 在NgModule中注冊提供商
@NgModule({
imports:[],
declarations:[],
providers:[ HeroService ]
})
2.在組件中注冊提供商
@Component({
selector:'my-heros',
template:`<h2></h2>`
providers:[ HeroService ]
})
服務
在寫一個服務時必須注意@Injectable() 是必不可少的,除非你不打算注入這個服務,因為@Injectable()相當于C++中的new()
import { Injectable } from '@angular/core';
@Injectable()
export class HeroService {
}
如何使用已注冊的服務?在組件或者服務的構造函數中注入即可
constructor(
public heroService: HeroService
) {}
所有被注入的服務在angular中總是單例的,肯定有童鞋要問了,那我需要多實例的怎么辦?
angular應用程序有多個依賴注入器,組成了一個與組件樹平行的樹狀結構,所以可以在任何組件級別提供和建立服務,當組件申請一個依賴時,angular會從該組件本身的注入器開始,沿著依賴注入器的樹向上查找,所以要實現多實例,在組件內注入服務即可
@host @Optional
有時候我們想限定依賴查找方式,比如只想讓組件向上搜索到宿主組件,可以使用@host,當組件一層層向上查找沒有找到注冊的服務時,就會拋出錯誤,如果不想拋出錯誤,可以使用@Optional,它會把注入參數設置為null
import { Host } from '@angular/core';
constructor( @Host heroService: HeroService ) {}
import { Optional } from '@angular/core';
constructor( @Optional heroService: HeroService ) {}
總結
angularjs中依賴注入涉及的知識面很多,本文只是作了一個簡單介紹,更詳細內容參考官方文檔和API