原文鏈接:https://angular-university.io/course/getting-started-with-angular2
在實際使用Angular依賴注入系統時,你需要知道的一切都在本文中。我們將以實用易懂並附帶示例的形式解釋它的所有高級概念。
Angular最強大、最獨特的功能之一就是它內置的依賴注入系統。
大多數時候,依賴注入就那么工作着,我們使用它,幾乎不會想到要歸功於它的便利且直觀的Angular API。
但也有些時候,我們也許需要深入研究一下依賴注入系統,手動配置它。
這種對依賴注入的深入了解,在下面的情況下是必須的:
- 為了解決一些古怪的依賴注入錯誤
- 為單元測試手動配置依賴
- 為了理解某些第三方模塊的不尋常的依賴注入配置
- 為了創建一個能夠在多個應用中裝載跟使用的第三方模塊
- 為了以一個更加模塊化的方式來設計你的應用
- 為了確保你的應用中的各個部分很好的互相獨立,不影響彼此
在本指南中,我們將准確理解Angular依賴注入是如何工作的,我們將涵蓋它的所有配置項,並學習何時以及為什么使用那些特性。
我們將以一種非常實用並易於理解的方法來實現這一點,從頭開始實現我們自己的提供商(provider)和注入令牌(injection token)。作為一個練習,使用基於示例的方式涵蓋所有的特性。
隨着時間的推移,作為Angular開發者,對Angular依賴注入系統的深入理解對你來說將是非常彌足珍貴的。
內容一覽
在本文中,我們將覆蓋以下主題:
- 對依賴注入的介紹
- 如何從頭開始在Angular中設置依賴注入
- 什么是Angular依賴注入提供商(provider)?
- 如何編寫我們自己的提供商?
- 對注入令牌的介紹
- 如何手動配置一個提供商?
- 使用類名作為注入令牌
- 提供商的簡化配置:useClass
- 理解Angular的多值依賴
- 何時使用提供商
useExisting
- 理解Angular的分層依賴注入
- 分層依賴注入的優勢是什么
- 組建分層依賴注入 - 一個示例
- 模塊分層依賴注入 - 一個示例
- 模塊依賴注入vs組建依賴注入
- 配置依賴注入解決機制
- 理解
@Optional
裝飾器 - 理解
@SkipSelf
裝飾器 - 理解
@Self
裝飾器 - 理解
@Host
裝飾器 - 什么是可搖樹(Tree-Shakeable)的提供商?
- 通過一個示例理解可搖樹的提供商
- 總結
本文是我們正在進行的Angular核心特性系列的一部分,你可以從這里找到所有的文章。
那么話不多說,讓我們開始學習關於Angular依賴注入的所有必須知道的內容吧!
對依賴注入的介紹
那么依賴注入具體是什么呢?
當你正在開發系統中的一個更小的部分時,比如一個模塊或者一個類,你將需要一些外部的依賴。
舉個例子,像其他的依賴一樣,你可能需要一個HTTP服務去做調用后端。
每當需要它們的時候,你也許甚至會嘗試在本地創建屬於你自己的依賴。像下面這樣:
export class CoursesService() {
http: HttpClient;
constructor() {
this.http = new HttpClient(... dependencies needed by HTTPClient ...);
}
...
}
這看起來像是一個簡單的解決辦法,但是這段代碼有個問題:非常難於測試。
因為這段代碼知道本身的依賴,並直接創建了它們。你無法將實際的HTTP客戶端替換為一個模擬HTTP客戶端,也無法替換為單元類。
注意這個類不僅知道如何創建自己的依賴,還知道它的依賴的依賴,意味着它也知道HTTPClient的依賴。
按照這段代碼的寫法,基本上無法在運行時將這個依賴替換為其他可選內容,比如:
- 出於測試目的
- 也因為你可能需要在不同的運行時環境中使用不同的HTTP客戶端,比如在服務器上和在瀏覽器中。
把這個跟相同類的一個變更版本進行比較,它用了依賴注入:
@Injectable()
export class CoursesService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
...
}
正如你在這個新版本中看到的,這個類無法知道如何創建它的http依賴。
這個新版的類簡單地從構造函數的輸入參數中接受它所需要的所有的依賴,就是這樣!
這個新版本的類只是知道如何使用它的依賴去實現具體的任務,但是它不知道依賴的內部是如何工作的,依賴是如何被創建的,也不知道依賴的依賴是什么。
用於創建依賴的代碼已經從當前類中移除,被放在了你代碼庫的某個地方,歸功於@Injectable()
裝飾器的使用。
有了這個新的類,可以非常簡單地:
- 為了測試的目的,替換某個依賴的實現
- 支持多運行時環境
- 在使用了你的服務作為第三方的代碼庫中,提供服務的新版本···
這種只從輸入中接收你的依賴,不知道它們內部是如何工作,以及如何創建的技術,就叫做依賴注入,它是Angular的基礎。
現在讓我們來學習Angular依賴注入具體是如何工作的。
如何從頭開始在Angular中設置依賴注入?
理解Angular中依賴注入的最好的方法,就是從頭開始,取一個簡單的TypeScript類,不應用任何的裝飾器到該類,然后手動把它轉變為Angular可注入服務。
比聽起來還要簡單。
讓我們從一個簡單的服務類開始,沒有任何的@Injectable()
裝飾器應用到該類:
export class CoursesService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
...
}
我們可以看到,這不過是一個簡單的TypeScript類,它期望從構造函數中注入一些依賴。
但其實這個類根本沒有途徑聯結到Angular依賴注入系統。
讓我們來看下,把這個類當作一個依賴注入到另外一個類中,會發生什么:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
...
}
...
}
我們可以看到,我們嘗試注入這個類的一個實例當作依賴。
但是我們的類沒有聯結到Angular的依賴注入系統,所以我們程序中的哪一塊會知道如何通過調用CoursesService
構造函數來創建這個類的實例,然后當作依賴傳入呢?
答案很簡單:不可能!而且我們會得到一個錯誤!
NullInjectorError: No provider for CoursesService!
注意錯誤信息:很明顯缺少某個被稱為provider的東西。
你可能之前見過類似的信息,在開發中時有發生。
現在讓我們來理解一下這個信息具體是什么意思,以及如何解決它。
什么是Angular依賴注入提供商?
錯誤信息“沒有提供商”僅僅表示Agnular依賴注入系統無法實例化一個給定的依賴,因為它不知道如何創建它。
為了讓Angular知道如何創建一個依賴,像是比如在CourseCardComponent
的構造函數中注入CoursesService
的實例,它需要知道什么可以被稱為提供商工廠函數。
提供商工廠函數就是一個簡單的函數,Angular可以調用它來創建依賴,很簡單:它就是一個函數。
那個提供商工廠函數可以使用我們即將談到的一些簡單的約定方式被Angular隱式創建。這個實際上就是通常我們的大部分依賴發生的情況。
不過根據需要,我們也可以自主編寫那個函數。
在任何情況下,必須要理解的是,在你應用程序的每一個依賴中,讓它成為一個服務,一個組件,或者其他什么,在某個地方有一個簡單的函數正在被調用,它知道如何創建你的依賴。
如何編寫我們自己的提供商?
為了真正理解一個提供商是什么,讓我們為CoursesService
類編寫我們自己的提供商工廠函數:
function coursesServiceProviderFactory(http:HttpClient): CoursesService {
return new CoursesService(http);
}
正如你所看到的,這就是一個普通的函數,它接收CoursesService
需要的任何依賴項作為輸入。
這個提供商工廠函數接着手動調用CoursesService
的構造函數,傳入所有需要的依賴項,然后返回CoursesService
的新的實例作為輸出。
那么任何時候Angular的依賴注入系統需要一個CoursesService
的實例,它僅僅只需要調用這個函數!
這看起來很簡單,但問題是Angular依賴注入系統暫時還不知道這個函數。
更為重要的是,即使Angular知道這個函數,它如何會知道要調用它去注入這個特殊的依賴項呢:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
...
}
...
}
我的意思是,沒法讓Angular將被注入的CoursesService
的實例跟這個提供商工廠函數關聯起來,對吧?
介紹注入令牌
那么Angular如何知道在哪里注入什么,還有提供商工廠函數要調用什么去創建哪個依賴項?
Angular需要能夠以某種方式歸類依賴項,為了識別一個給定的依賴項集合是屬於同樣類型。
為了獨一無二地識別一組依賴項,我們可以定義一些東西當作Angular的注入令牌。
下面是我們如何為我們的CoursesService
依賴項手動創建我們的注入令牌:
export const COURSES_SERVICE_TOKEN =
new InjectionToken<CoursesService>("COURSES_SERVICE_TOKEN");
這個注入令牌對象將在依賴注入系統中被用來明確地識別我們的依賴項CoursesService
。
這個依賴注入令牌是一個對象,所以它是獨一無二的,不像比如說字符串。
因此,可以使用這個令牌對象來唯一地識別一組依賴項。
那么我們如何使用它呢?
如何手動配置一個提供商?
現在我們已經擁有了提供商工廠函數還有注入令牌,我們可以在Angular依賴注入系統中配置一個提供商,它會知道如何根據需要創建CoursesService
的實例。
提供商本身就是一個簡單的配置對象,我們把它傳給一個模塊或者組件的providers
數組中:
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [
{
provide: COURSES_SERVICE_TOKEN,
useFactory: coursesServiceProviderFactory,
deps: [HttpClient]
}
]
})
export class CoursesModule { }
正如我們所見,這個手動配置的提供商需要定義下列項:
useFactory
:它應該包含對提供商工廠函數的一個引用,當需要創建依賴項和注入它們的時候,Angular會調用這個提供商工廠函數provide
:它包含了關聯到這種依賴項的注入令牌。注入令牌會幫助Angular決定何時使用或不使用一個給定的提供商工廠函數deps
:這個數組包含了任何useFactory
函數需要運行的依賴項,在這個例子中是HTTP client
那么現在Angular知道了如何創建CoursesService
的實例,對吧?
讓我們看看假如我們嘗試注入一個CoursesService
的實例到我們的應用程序中,會發生什么:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
...
}
...
}
我們也許有點驚訝,看到同樣的錯誤又發生了:
NullInjectorError: No provider for CoursesService!
那么這里發生什么了?我們不是剛剛定義了提供商嗎?
對,我們是定義了提供商,但是當Angular試圖創建這個依賴項時,它無法知道它是否需要使用我們特定的提供商工廠函數,對吧?
那么我們如何做出那個關聯呢?
我們需要顯式地告訴Angular它應該使用我們的提供商來創建這個依賴項。
我們可以在任何CoursesService
被注入的地方使用@Inject
注釋來做這個:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor( @Inject(COURSES_SERVICE_TOKEN) private coursesService: CoursesService) {
...
}
...
}
正如我們所看到的,顯式使用@Inject
裝飾器允許我們告訴Angular,為了創建這個依賴項,它需要使用關聯到COURSES_SERVICE_TOKEN
的指定的提供商。
這個注入令牌從Angular的視角,獨一無二地識別出一個依賴項類型,這就是依賴注入系統如何知道使用哪個提供商的。
因此現在Angular知道了調用哪個提供商工廠函數去創建正確的依賴項,它就是這樣做了。
然后有了這些,我們的應用程序現在正確地工作着,不再有錯誤了!
我想現在你對Angular的依賴注入系統是如何工作的應該有了一個很好的理解,不過我猜你可能會在想:
但是為什么我從來沒有手動配置過提供商呢?
你看,即使一般你不必自己手動配置提供商工廠函數或者注入令牌,這些其實都在底層發生着。
對於你的應用程序中每個單獨的依賴項類型而言,服務、組件后者其他的,永遠都會有一個提供商,並且永遠都有一個注入令牌,后者其他的機制來獨一無二地識別一個依賴類型。
這是有意義的,因為你的類的構造函數需要在你的系統的其他地方被調用,Angular總是需要知道創建哪個依賴項,對吧?
因此即使當你用簡化的方式來配置你的依賴項的時候,底層永遠都有一個提供商。
為了更好地理解這點,讓我們逐漸簡化我們提供商的定義,一直到我們碰到那些你更加熟悉的地方。
使用類名作為注入令牌
Angular依賴注入系統的最有趣的特性之一,就是你可以使用在JavaScript運行時能保證唯一的任何事物,來識別一個依賴項類型,它不必非要是一個顯式的注入令牌對象。
舉例來說,在JavaScript的運行時,構造函數用來代表類名,指向某個函數的引用比如說它的名字,被保證在運行時是唯一的。
類名可以在運行時被它的構造函數唯一地表示,因為它保證是唯一的,它可以被用來作為注入令牌。
因此我們可以利用這個強大的特性,稍微簡化我們提供商的定義:
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [
{
provide: CoursesService,
useFactory: coursesServiceProviderFactory,
deps: [HttpClient]
}
]
})
export class CoursesModule { }
正如我們所見,我們手動創建的用來識別我們的依賴項類型的注入令牌COURSES_SERVICE_TOKEN
,已經不再需要了。
其實,我們已經把那個對象從我們的代碼庫中一並移除了,因為在服務類的特定用例中,我們可以使用類名本身來識別依賴項類型!
不過如果不做更多修改,我們嘗試運行程序的話,我們可能又會得到錯誤no provider
。
為了再次正常工作,你還需要使用CoursesService
的構造函數來識別你需要哪個依賴項:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor( @Inject(CoursesService) private coursesService: CoursesService) {
...
}
...
}
然后有了這個,Angular知道了要注入哪個依賴項,一切又如期工作了!
所以好消息是,在大多數情況下,我們無需顯式地創建一個注入令牌。
現在讓我們來看下我們如何進一步地簡化我們的提供商。
簡化提供商的配置:useClass
不同於使用useFactory
顯式地定義一個提供商工廠函數,我們有其他辦法告訴Angular如何實例化一個依賴項。
在提供商的情況下,我們可以使用useClass
屬性。
在這種方式下Angular會知道我們傳入的值是一個合法的構造函數,Angular可以簡單地使用new
操作符來調用它:
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [
{
provide: CoursesService,
useClass: CoursesService,
deps: [HttpClient]
}
]
})
export class CoursesModule { }
這已經相當地簡化了我們的提供商,因為我們不需要自己手動編寫一個提供商工廠函數。
useClass
的另外一個超級便利的特性就是,對於這個依賴項類型,基於TypeScript的類型注釋,Angular會在運行時推斷注入令牌。
這意味着,有了useClass
依賴項,我們甚至不再需要Inject
裝飾器,這可以為什么你極少看到它:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
...
}
...
}
那么Angular是如何知道注入哪個依賴項的呢?
Angular可以通過檢查被注入屬性的類型來決定,這里是CoursesService
,然后使用那個類型為那個依賴項決定一個提供商。
正如我們所見,類依賴項使用起來更為方便,相對於不得不顯示地使用@Inject
!
在useClass
提供商的特定情況下,我們可以更加簡化這一切。
無需手動定義提供商對象,我們可以簡單地傳入類本身的名字作為合法的提供商配置項:
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [
CoursesService
]
})
export class CoursesModule { }
Angular會確定這個提供商是一個構造函數,因此Angular會檢查這個函數,它會創建一個工廠函數確定必要的依賴項,然后根據需要創建這個類的實例。
這是基於函數的名稱隱式地發生的。
這是你通常在大多數情況下用到的記號方法,它超級簡單易用!
有了這個簡化的記號方法,你甚至不會意識到在幕后有提供商和注入令牌。
不過要注意,僅僅像這樣設置你的提供商是不會工作的,因為Angular不會知道如何查找這個類的依賴項(記住屬性deps
)。
為了讓它工作,你仍然需要應用Injectable()
裝飾器到這個服務類中:
@Injectable()
export class CoursesService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
...
}
這個裝飾器將會告訴Angular通過在運行時檢查構造函數參數的類型來嘗試查找該類的依賴項!
因此正如你所見,這個相當簡化了的記號方法,就是我們通常使用Angular依賴注入系統的方式,甚至沒有考慮到在底層使用的具體細節。
有件事要記住,就是useClass
選項將不會與接口名稱工作,它只工作於類的名稱。
這是因為接口只是TypeScript語言的僅在編譯時的結構,因此接口不會存在於運行時。
這意味着接口名稱,不像類名(通過它的運行時構造函數),不會被用來唯一地識別依賴項類型。
除了提供商、依賴項和注入令牌的基本概念以外,還有一些其他的你必須記住的關於Angular依賴注入系統的東西。
理解Angular的多值依賴
我們系統中大多數的依賴項都只對應於一個值,比如一個類。
但是有一些情形下,我們想要多個不同值的依賴項。
一個你應該已經遇到的很常見的例子就是表單控件的值的訪問器。
這些是特殊的表單指令,它們綁定到一個給定的表單控件,讓表單控件的值對於表單模塊(Forms module)是可見的。
問題是不會僅有一個像這樣的指令,有很多。
但是如果全部獨立地配置這些依賴項,那將很不實用,因為通常你想要一次性的一起訪問它們。
因為解決辦法就是擁有一個特殊的依賴項類型,它會接收多個值,不僅僅一個,關聯到相同的依賴注入令牌。
在表單控件的值訪問器的情況下,那個特殊的令牌就是NG_VALUE_ACCESSOR
注入令牌。
舉個例子,下面是一個自定義表單控件的示例,它想把自己注冊為一個控件的值訪問器:
@Component({
selector: 'choose-quantity',
templateUrl: "choose-quantity.component.html",
styleUrls: ["choose-quantity.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi:true,
useExisting: ChooseQuantityComponent
}
]
})
export class ChooseQuantityComponent implements ControlValueAccessor {
}
注意在這里我們為NG_VALUE_ACCESSOR
注入令牌定義了一個提供商。
但是如果我們不使用multi
屬性,這個注入令牌的值將會被覆寫。(后面會提到)
但是因為multi
被設置為true,我們實際上把值添加到了依賴項的值的數組中,而不是覆寫它。
任何需要所有控件值的訪問器的組件或者指令,將會通過請求NG_VALUE_ACCESSOR
的注入來接收它們。
這將對應於包含所有標准表單控件值訪問器的數組,以及我們自定義的數組。
什么時候使用useExisting
提供商
還要注意為了創建提供商,useExisting
選擇的使用。
當我們想基於其他已經存在的提供商創建一個提供商時這個選項是很有用的。
在這個例子中,我們僅僅想用一種簡單的方式通過指明ChooseQuantityComponent
的類的名稱來定義一個提供商,我們已經學習過可以使用這個類名作為提供商。
useExisting
功能也對一個已經存在的提供商定義別名很有用。
現在關於提供商和注入令牌是如何工作的,我們已經有了一個很好的理解,讓我們談談Angular依賴注入系統的另一個基本方面。
理解Angular的分層依賴注入
跟之前的AngularJS版本不同,Angular的依賴注入系統可以說是分層的。
那么這具體是什么呢?
如果你注意到了,在Angular中你可以在多個地方為你的依賴項定義提供商:
- 在模塊層級
- 在組件層級
- 或者甚至在指令層級!
那么在所有這些不同的地方定義提供商,有什么區別呢?它是如何工作的?以及為什么會有那些不同的選擇呢?
你能在多個地方定義提供商,是因為Angular的依賴注入系統是分層式的。
你看,如果你在某處需要一個依賴項,比如你需要注入一個服務到組件中,Angular首先會嘗試在組件的提供商列表中查找那個依賴項的提供商。
如果Angular在組件本身的層級中沒有找到需要該依賴項的提供商,那么Angular會嘗試在父組件中查找那樣的提供商。
如果找到了提供商,它會實例化並使用它,但如果沒有,它會詢問父組件的父組件是否有它需要的提供商,以此類推。
這個過程會重復到應用程序的根組件為止。
如果在此過程中沒有找到提供商,你知道會發生什么的:對,我們得到我們的老朋友“No provider found”信息。
這個在組件樹中一直向上查找正確的提供商的過程,就是依賴解析,因為它遵循我們組件樹的分層式結構,我們說Angular依賴系統是分層式的。
我們也需要知道為何這個特性是有用的。
分層式依賴注入的好處是什么?
Angular典型地被用來構建大型應用程序,在某些情況下可能會相當大。
管理這個復雜度的一個方法就是把應用程序分解為許多封裝好的小模塊,這些模塊本身又分解為定義良好的組件樹。
頁面中這些不同的部分需要特定的服務還有其他的依賴項來工作,這些依賴項也許會或者不會想要與應用程序中其他部分共享。
我們可以想象頁面中一個完全隔離的部分,與應用程序的其他部分相比,它以一種完全獨立的方式工作,具有一系列服務的本地副本和它需要的其他依賴項。
我們想要確保這些依賴項保持私有,並且無法被應用程序的其他地方所接觸到,這樣來避免BUG和其他維護的問題。
在應用程序中我們的獨立的部分使用的一些服務,可能與其他部分共享,或者與組件樹中更深一層的父組件分享,同時其余依賴項是私有的。
分層式依賴注入系統允許我們實現這個!
利用分層式依賴注入,我們可以隔離應用程序的各個部分,給它們不與應用程序中其他部分共享的私有的依賴項,我們可以讓父組件僅與子組件共享某些依賴項,但不與組件樹中其他部分共享,以此類推。
分層式依賴注入系統允許我們以更模塊化的方式構建我們的系統,允許我們僅在需要的時候在應用程序的不同部分之間共享依賴項。
這個最初的解釋是一個很好的起點,但是要真正理解這一切是如何工作的,我們需要一個完整的示例。
通過示例理解組件分層式依賴注入
比如,讓我們用我們的CoursesService
類。如果我們嘗試在Angular的組件樹中到處注入這個類的話,會發生什么呢?
會發生啥?我們會得到這個服務的多個實例嗎?或者只有一個實例?它是如何工作的呢?
為了幫助我們理解這些,讓我們給CoursesService
的每個實例一個唯一識別符,那樣我們可以更好的理解發生了什么:
let counter = 0;
@Injectable()
export class CoursesService() {
constructor(private http: HttpClient) {
counter++;
this.id = counter;
}
...
}
現在我們來創建一個簡單的組件層,把CoureseService
注入到多個地方,然后看看會發生什么!
讓我們來創建一個根應用程序組件,在這個組件的模板中使用一個子組件course-card
。
下面是根組件app.component.html
模板:
<div class="courses">
<course-card *ngFor="let course of courses "
[course]="course">
<course-image [src]="course.iconUrl"></course-image>
</course-card>
</div>
我們可以看到,這個組件內部在ngFor
循環中使用了course-card
組件。
下面是這個組件類文件app.component.ts
:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [
CoursesService
]
})
export class AppComponent {
constructor(private coursesService: CoursesService) {
console.log(`App component service Id = ${coursesService.id}`);
}
...
}
注意我們把CoursesService
添加到根組件的提供商中,我們還把獲得的這個服務的實例的唯一識別符打印到控制台中。
最后,下面是course-card
組件的樣子,這是course-card.component.ts
文件:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css'],
providers: [
CoursesService
]
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
console.log(`course card service Id = ${coursesService.id}`);
}
...
}
注意這里我們也把CoursesService
加入到了這個組件的提供商的列表中,並打印這個服務實例的id到控制台。
如果現在啟動應用程序,你覺得會發生什么呢?
你認為有多少個CoursesService
的實例會被創建呢?哪些服務被注入到哪一個course-card
組件中呢?
下面是控制台的輸出:
App component service Id = 1
course card service Id = 2
course card service Id = 3
course card service Id = 4
course card service Id = 5
course card service Id = 6
course card service Id = 7
course card service Id = 8
course card service Id = 9
course card service Id = 10
course card service Id = 11
所以這里發生了啥?讓我們拆解發生的內容。
那么看起來應用程序的根組件app.component.ts
是第一個創建了服務實例的,因此它有Id = 1。
當嘗試獲取CoursesService
依賴項的時候,根組件首選查看它自己的提供商列表,然后它找到匹配的提供商並使用了它。
但是在course-card
組件的內部發生了什么呢?
與程序的根組件不同,course-card
組件有10個實例被創建了,並顯示在了屏幕上。
每個course-card
的實例需要一個CoursesService
,所以它嘗試通過查找自身的提供商列表來實例化服務。
每個course-card
組件實例在它的私有提供商列表中找到了匹配的提供商,然后用它創建了一個新的CoursesService
實例,注入到了它的依賴項中。
這表示每個course-card
實例創建了它們各自獨立的CoursesService
實例,不需要向父組件索要CoursesService
的實例。
有10個course-card
實例,每個都擁有自己的私有的CoursesService
實例,這就解釋了上面的日志!
注意,這些私有的CoursesService
實例跟它們的course-card
實例的組件生命周期關聯起來了。
所以如果course-card
的組件實例被銷毀了,它們相應的CoursesService
實例也會被垃圾回收。
不過大多數時候CoursesService
是一個無狀態的類(本例的計數器除外),因為沒有必要創建這么多實例。
我們更想要的是只有一個CoursesService
實例,由根組件創建,並且與它的所有的子組件共享。
我們可以通過從CourseCardComponent
的提供商列表中移除提供商CoursesService
:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css'],
providers: []
})
export class CourseCardComponent {
constructor(private coursesService: CoursesService) {
console.log(`course card service Id = ${coursesService.id}`);
}
...
}
注意這個組件的空的提供商列表,我們甚至也可以把providers
屬性一並移除了。
如果我們這么做,然后再跑一次我們的程序,下面是我們得到的:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
正如我們所看到的,我們的程序現在正如預期,只有一個CoursesService
的實例了。
那么現在我們已經搞懂了只在組件樹的情況下分層式依賴注入是如何工作的了 - Angular先在組件中查找提供商的匹配,然后掃描它所有的父組件。
那模塊呢?模塊也可以擁有它們自己的提供商,對吧?
通過示例理解模塊分層式依賴注入
讓我們把這些組件先這樣放着,表示:
- 應用的根組件有一個
CoursesService
提供商 course-card
組件沒有屬於自己的私有的提供商
注意,course-card
組件是模塊CoursesModule
的一部分。
那么如果我們保持這些組件的提供商不變,在模塊級別再添加兩個提供商,會發生什么呢?
首先讓我們在CoursesModule
層級新增一個提供商:
@NgModule({
imports: [
CommonModule
],
declarations: [
CourseCardComponent
],
exports: [
CourseCardComponent
],
providers: [
CoursesService
]
})
export class CoursesModule { }
這是一個特性模塊,它是應用程序根模塊的一部分。
現在讓我們在根模塊級別在同樣新增一個提供商:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
CoursesModule
],
providers: [
CoursesService
],
bootstrap: [AppComponent]
})
export class AppModule {}
那么現在在應用程序中我們又有了兩個額外的提供商,總共有三個:
- 一個在根組件
AppComponent
級別 - 一個在特性模塊
CoursesModuls
級別 - 一個在應用程序根模塊
AppModule
級別
如果我們現在運行程序,你覺得會發生什么?
下面是控制台輸出:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
是完全相同的輸出!
那這里發生什么了呢?
模塊vs組件 依賴注入層次結構
實際上正在發生的是有兩個獨立的依賴注入層次結構:
- 有一個組件級別的層次結構,遵循頁面上的組件樹結構
- 不過還有一個模塊級別的獨立的注入層次結構
組件級別的層次結構優先於模塊注入層次結構!
所以當Angular為了組件或者服務嘗試查找一個依賴項的時候,如果可能的話,它會先嘗試通過組件級別的提供商創建依賴項。
如果Angular追蹤組件一直向上到達根組件,都沒有找到匹配的提供商,仍然什么都沒找到的話,只有這個時候Angular才會嘗試在模塊的層級結構的級別上查找匹配的提供商。
然后Angular從當前模塊的提供商們開始查找匹配的提供商。
如果沒有找到,Angular會嘗試到當前模塊的父模塊中查找,以此類推。直到應用程序的根模塊。
由兩個分開的注入層次結構組成的系統,允許我們在兩個維度模塊化我們的應用程序:
- 通過使用組件注入層次結構,我們可以在組件樹的特定部分提供某些依賴項,從而模塊化應用程序
- 不過我們可以同樣通過使用模塊注入層次結構,模塊化和創建一個服務的多個獨立版本,只在程序的某些模塊中使用它們
現在我們已經熟悉了層級化依賴注入系統如何工作的主要概念,現在讓我們學習一下如何更近一步配置它的依賴解析機制。
配置依賴注入解析機制
正如我們已經學習到的,Angular組件依賴注入解析機制總是從當前組件開始,然后向上掃描匹配的提供商一直到應用程序的根組件,它找不到匹配的依賴項就拋出一個錯誤。
但如果我們想稍微調整一下這個行為呢?
理解@Optional
裝飾器
舉個例子,如果因為這個依賴項也許不需要,你可能有一個替代項來使用它,我們想阻止最后的錯誤被拋出呢?我們可以使用@Optional
裝飾器讓依賴項變為可選擇:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css']
})
export class CourseCardComponent {
constructor(@Optional() private coursesService: CoursesService) {
...
}
...
}
如果沒有找到這個依賴項的提供商的話,這會阻止錯誤被拋出。但是你需要確定你的組件檢查了這個依賴項是否存在,如果不存在,則提供替代方案。
理解@SkipSelf
操作符
你也可以稍微調整一下依賴解析機制開始尋找匹配提供商的地方。
舉個例子,你可能在組件級別有一些服務的提供商,你想把它們提供給子組件使用,但是呢這個組件本身也需要這個服務的一個實例來工作,並且它需要從它的父組件而不是自身的提供商列表中來獲取它。
補充說明一下,這肯定是非常罕見的情況!
但假如你曾經碰到這種情況了,你可以通過使用@SkipSelf
裝飾器跳過本地的組件提供商,然后從直接從父組件開始匹配,一直到達根組件:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css'],
providers: [
CoursesService
]
})
export class CourseCardComponent {
constructor(@SkipSelf() private coursesService: CoursesService) {
...
}
...
}
在我們的程序中,這會導致跳過本地的CoursesService
提供商,意味着本地的那個提供商僅對子組件course-card
可用。
如果運行我們的程序,下面是我們會得到的日志:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
正如我們所見,CourseCardComponent
級別的提供商被跳過了,使用了CoursesModule
級別的提供商。
理解@Self
裝飾器
除了配置從哪里我們開始依賴項的匹配過程以外,我們還可以稍微調整一下匹配過程在何處結束。
舉個例子,如果我們希望組件只在它自身的提供商列表中查找某個依賴項,並跳過檢查所有的父組件,我們可以使用@Self
裝飾器來實現這個:
@Component({
selector: 'course-card',
templateUrl: './course-card.component.html',
styleUrls: ['./course-card.component.css'],
providers: [
CoursesService
]
})
export class CourseCardComponent {
constructor(@Self() private coursesService: CoursesService) {
...
}
...
}
這會兒,會采取CoursesService
的本地的提供商,在CoursesModule
級別的父級實例都被跳過。
下面是這種情況下我們程序的日志:
App component service Id = 1
course card service Id = 2
course card service Id = 3
course card service Id = 4
course card service Id = 5
course card service Id = 6
course card service Id = 7
course card service Id = 8
course card service Id = 9
course card service Id = 10
course card service Id = 11
正如我們所見,我們再次掉進了這樣的場景:組件的每個實例都擁有各自私有的服務實例了。
理解@Host
裝飾器
迄今為止我們一直在討論組件,已經它們是如何與依賴注入系統交互的,但指令呢?
因為組件只不過是一種特殊的指令,我們迄今為止學到的關於組件的東西都可以應用到指令上。
不過有一個特殊的情況:假設這個組件有一個由自身提供商創建的私有的服務實例。
現在或許有一個指令應用到了那個組件上,指令在同一個模塊中,它被設計為跟那個特別的組件緊密交互。
因為該指令與這個組件緊密耦合,它可能會訪問與組件關聯的一些私有的服務,而不是其他服務。
假設有一個簡單的HighlightedDirective
,它被用來在視覺上高亮它所應用的一個課程卡片。
假設設計這個指令,用來與CourseCardComponent
緊密交互,它要訪問組件中CoursesService
的私有實例。
該指令可以用下面的方式來訪問私有的組件內的服務:
@Directive({
selector: '[highlighted]'
})
export class HighlightedDirective {
constructor( @Host() private coursesService: CoursesService) {
console.log('coursesService highlighted ' + coursesService.id);
}
...
}
使用@Host
裝飾器來配置Angular應該在何處停止查找依賴項,與@Self
做的事情相似。
但在這個例子中,該裝飾器僅在指令中使用,它表示Angular僅在指令的宿主組件的提供商列表中查找匹配的提供商,不會去其他地方查找。
現在讓我們把該指令應用到我們應用程序中的每一個course-card
組件中:
<div class="courses">
<course-card *ngFor="let course of courses " highlighted
[course]="course">
<course-image [src]="course.iconUrl"></course-image>
</course-card>
</div>
注意使用highlighted
的屬性,它為每個CourseCardComponent
實例分配一個HighlightedDirective
同伴實例。
如果現在我們運行應用程序,我們會得到下面的控制台輸出:
App component service Id = 1
course card service Id = 2
coursesService highlighted 2
course card service Id = 3
coursesService highlighted 3
course card service Id = 4
coursesService highlighted 4
course card service Id = 5
coursesService highlighted 5
course card service Id = 6
coursesService highlighted 6
course card service Id = 7
coursesService highlighted 7
course card service Id = 8
coursesService highlighted 8
course card service Id = 9
coursesService highlighted 9
course card service Id = 10
coursesService highlighted 10
course card service Id = 11
coursesService highlighted 11
正如我們所看到的,每個HighlightedDirective
的實例都可以訪問該指令所應用的同伴CoursesService
的私有實例。
至此,我們已經全面覆蓋了我們能夠配置Angular依賴解析機制的多種途徑。
現在我們很好的理解了分層依賴注入是如何工作的,讓我們來談談Angular依賴注入系統的另一個強大的特性。
什么是可樹抖動(Tree-Shakeable)的提供商?
到現在為止我們使用的作為示例的提供商都缺少一個很重要的屬性:它們是不可樹抖動的。
那么這是什么意思呢?
一直以來,那些我們已經定義了的提供商都是通過使用providers
屬性顯式地添加到組件或者模塊的提供商列表中的。
但這種方法存在一個實際問題。
假設一個應用程序在某個模塊中有一個依賴項,該模塊又導入了其他模塊,然后該模塊提供了一個服務類。
現在假設這個被導入的服務類,實際上在任何地方都沒有被用到,無所謂什么原因!
並且這確實是一個非常普遍的場景。
假如你在使用比如AngularFire或者其他大的擁有海量功能的第三方模塊,這些模塊包含了各種在應用程序中你想要或者不想要的服務。
你也許會用到一些服務,但很可能不會用到全部服務。
想法就是我們應該能夠導入一個模塊,但如果用不到某些服務的話,我們沒必要在生產包中包含它們。
那么如何做到呢?
什么可樹抖動?
當使用Angular CLI構建我們的生產包時,它會盡可能嘗試“tree shake”那些沒必要出現在包中的代碼,為了盡量減輕包的大小。
CLI會嘗試靜態檢查代碼中的TypeScript依賴,然后確定某個依賴是否被使用到。
如果它發現某個依賴項沒有被使用到,它會把那個依賴從包中移除,這樣一來就減輕了包的大小。
但如果我們直接在模塊或者組件的提供商屬性中給這個模塊添加未被使用的服務,我們需要首先使用TypeScript import直接導入那個模塊或者服務。
這個TypeScript import將有效地防止搖樹服務刪除未使用的服務。
樹抖動器看到這個服務通過TypeScript import被顯式地導入了,於是它就錯誤地認為這個服務被使用着,然后它將不會從生產包中移除這個服務。
那么我們如何來解決這個問題呢?
我們需要可搖樹的提供商。
通過一個示例理解可搖樹的提供商
我們要做的是定義CoursesService
作為模塊CoursesModule
的一部分。
在我們的應用程序中,它完全可能成為我們引入的一個第三方模塊。
我們想以某種方式為CoursesService
定義一個提供商,這種方式就是如果它被任何引用了CoursesModule
的人使用了的話,將把它包含在最終的包中。
但是如果因為某些原因,應用程序引入了CoursesModule
,但是最終都沒有用到CoursesService
,那么這個服務就不應該被包含到包中。
為了實現這個,第一件要做的事就是從CoursesModule
的提供商列表中移除CoursesService
。
@NgModule({
imports: [
CommonModule
],
declarations: [
CourseCardComponent
],
exports: [
CourseCardComponent
]
})
export class CoursesModule { }
但是現在我們不會得到“provider not found”的錯誤信息嗎?
是的,因為已經沒有定義了的提供商了。
下面是我們如何在CoursesModule
級別為CoursesService
定義一個提供商,而無需在courses.module.ts
文件中引用它:
@Injectable({
providedIn: CoursesModule
})
export class CoursesService() {
constructor(private http: HttpClient) {
}
...
}
你可以看到,我們反轉了依賴項的順序,我們在CoureseService
的內部引入了CoursesModule
,並且使用@Injectable
裝飾器在服務類自身中定義了模塊級別的提供商。
用這種方式,CoursesModule
根本沒有引入服務類,因此如果服務沒有被用到,這個服務將按照預期從生產包中被搖樹掉(移除)。
注意,@Injectable
裝飾器允許我們以各種方式定義提供商,不僅僅模塊級別的提供商。
我們同樣可以訪問選項useClass
,useValue
,useExisting
以及deps
,就像在模塊或者組件級別定義提供商時候一樣。
使用providedin
,我們不僅可以定義模塊級別的提供商,通過讓服務在根模塊依賴注入處可用,還可以將服務提供給其他的模塊。
@Injectable({
providedIn: "root"
})
export class CoursesService() {
constructor(private http: HttpClient) {
}
...
}
你應該很熟悉這個最常見的句法了。
使用這個句法,CoursesService
現在是應用程序范圍的單例,意味着在整個應用程序中,只有該服務的一個實例,這在我們的例子中是有意義的,因為我們的服務是無狀態的。
至此,本節就結束了,現在讓我們快速總結一下這篇文章中所學到的關鍵要點。
總結
我們已經知道,Angular依賴注入系統的背后在做着很多事情。
依賴系統是非常靈活的,它有很多強大的配置項。
它最常見的用法就是簡單地把一個類名丟進提供商列表中!
然而,當你盡量以模塊化方式設計你的應用程序的時候,詳細了解依賴注入系統的工作細節,將會非常有用!
依賴注入系統,因為它是分層的,而且因為它包含兩個獨立的注入層次(組件和模塊),所以允許你以非常細粒度的方式定義哪些依賴在應用程序的哪個部分是可見的,哪些依賴是隱藏的。
這些特性非常有用,特別是當你創建了一個第三方模塊,並且你想要發布的服務可能會在許多不同的應用程序中使用時,但如果你正在模塊化一個大型應用程序時也是如此。
盡管所有這些功能強大的特性都是可用的,但大多數時候默認的配置機制都能提供幫助,而且非常容易使用。
我希望你會喜歡本文,如果你想學習Angular的其他強大的核心特性,我推薦你們看下課程Angular Core Deep Dive,該課程中詳細涵蓋了依賴注入以及好多其他的特性。
此外,如果你有什么問題或意見,請在下方的評論中告訴我,我會回復你的。
如果你正在開始學習Angular,看一下這個課程Angular for Beginners Course。
-- The End --