Angular 中的注入器有一些規(guī)則,你可以利用這些規(guī)則來在應(yīng)用程序中獲得所需的可注入對(duì)象可見性。通過了解這些規(guī)則,可以確定應(yīng)在哪個(gè) NgModule
、組件或指令中聲明服務(wù)提供者。
Angular 中有兩個(gè)注入器層次結(jié)構(gòu):
ModuleInjector
層次結(jié)構(gòu) —— 使用 @NgModule()
或 @Injectable()
注解在此層次結(jié)構(gòu)中配置 ModuleInjector
。ElementInjector
層次結(jié)構(gòu) —— 在每個(gè) DOM 元素上隱式創(chuàng)建。除非你在 @Directive()
或 @Component()
的 providers
屬性中進(jìn)行配置,否則默認(rèn)情況下,ElementInjector
為空。
可以通過以下兩種方式之一配置 ModuleInjector
:
@Injectable()
的 providedIn
屬性引用 @NgModule()
或 root
。@NgModule()
的 providers
數(shù)組。搖樹優(yōu)化與 @Injectable()
使用
@Injectable()
的providedIn
屬性優(yōu)于@NgModule()
的providers
數(shù)組,因?yàn)槭褂?@Injectable()
的providedIn
時(shí),優(yōu)化工具可以進(jìn)行搖樹優(yōu)化,從而刪除你的應(yīng)用程序中未使用的服務(wù),以減小捆綁包尺寸。
搖樹優(yōu)化對(duì)于庫(kù)特別有用,因?yàn)槭褂迷搸?kù)的應(yīng)用程序不需要注入它。在
DI
提供者中了解有關(guān)可搖樹優(yōu)化的提供者的更多信息。
ModuleInjector
由 @NgModule.providers
和 NgModule.imports
屬性配置。ModuleInjector
是可以通過 NgModule.imports
遞歸找到的所有 providers
數(shù)組的扁平化。
子 ModuleInjector
是在延遲加載其它 @NgModules
時(shí)創(chuàng)建的。
使用 @Injectable()
的 providedIn
屬性提供服務(wù)的方式如下:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // <--provides this service in the root ModuleInjector
})
export class ItemService {
name = 'telephone';
}
@Injectable()
裝飾器標(biāo)識(shí)服務(wù)類。該 providedIn
屬性配置指定的 ModuleInjector
,這里的 root
會(huì)把讓該服務(wù)在 root ModuleInjector
上可用。
在 root
之上還有兩個(gè)注入器,一個(gè)是額外的 ModuleInjector
,一個(gè)是 NullInjector()
。
思考下 Angular 要如何通過 "main.ts" 中的如下代碼引導(dǎo)應(yīng)用程序:
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {...})
bootstrapModule()
方法會(huì)創(chuàng)建一個(gè)由 AppModule
配置的注入器作為平臺(tái)注入器的子注入器。也就是 root ModuleInjector
。
platformBrowserDynamic()
方法創(chuàng)建一個(gè)由 PlatformModule
配置的注入器,該注入器包含特定平臺(tái)的依賴項(xiàng)。這允許多個(gè)應(yīng)用共享同一套平臺(tái)配置。例如,無(wú)論你運(yùn)行多少個(gè)應(yīng)用程序,瀏覽器都只有一個(gè) URL 欄。你可以使用 platformBrowser()
函數(shù)提供 extraProviders
,從而在平臺(tái)級(jí)別配置特定平臺(tái)的額外提供者。
層次結(jié)構(gòu)中的下一個(gè)父注入器是 NullInjector()
,它是樹的頂部。如果你在樹中向上走了很遠(yuǎn),以至于要在 NullInjector()
中尋找服務(wù),那么除非使用 @Optional()
,否則將收到錯(cuò)誤消息,因?yàn)樽罱K所有東西都將以 NullInjector()
結(jié)束并返回錯(cuò)誤,或者對(duì)于 @Optional()
,返回 null
。
下圖展示了前面各段落描述的 root ModuleInjector
及其父注入器之間的關(guān)系。
雖然 root
是一個(gè)特殊的別名,但其它 ModuleInjector
都沒有別名。每當(dāng)創(chuàng)建動(dòng)態(tài)加載組件時(shí),你還會(huì)創(chuàng)建 ModuleInjector
,比如路由器,它還會(huì)創(chuàng)建子 ModuleInjector
。
無(wú)論是使用 bootstrapModule()
的方法配置它,還是將所有提供者都用 root
注冊(cè)到其自己的服務(wù)中,所有請(qǐng)求最終都會(huì)轉(zhuǎn)發(fā)到 root
注入器。
如果你在 AppModule
的 @NgModule()
中配置應(yīng)用級(jí)提供者,它就會(huì)覆蓋一個(gè)在 @Injectable()
的 root
元數(shù)據(jù)中配置的提供者。你可以用這種方式,來配置供多個(gè)應(yīng)用共享的服務(wù)的非默認(rèn)提供者。
下面的例子中,通過把 location
策略 的提供者添加到 AppModule
的 providers
列表中,為路由器配置了非默認(rèn)的 location
策略。
Path:"src/app/app.module.ts (providers)" 。
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy }
]
Angular 會(huì)為每個(gè) DOM 元素隱式創(chuàng)建 ElementInjector
。
可以用 @Component()
裝飾器中的 providers
或 viewProviders
屬性來配置 ElementInjector
以提供服務(wù)。例如,下面的 TestComponent
通過提供此服務(wù)來配置 ElementInjector
:
@Component({
...
providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent
在組件中提供服務(wù)時(shí),可以通過 ElementInjector
在該組件實(shí)例處使用該服務(wù)。根據(jù)解析規(guī)則部分描述的可見性規(guī)則,它也同樣在子組件/指令處可見。
當(dāng)組件實(shí)例被銷毀時(shí),該服務(wù)實(shí)例也將被銷毀。
組件是一種特殊類型的指令,這意味著 @Directive()
具有 providers
屬性,@Component()
也同樣如此。 這意味著指令和組件都可以使用 providers
屬性來配置提供者。當(dāng)使用 providers
屬性為組件或指令配置提供者時(shí),該提供程商就屬于該組件或指令的 ElementInjector
。同一元素上的組件和指令共享同一個(gè)注入器。
當(dāng)為組件/指令解析令牌時(shí),Angular 分為兩個(gè)階段來解析它:
針對(duì) ElementInjector
層次結(jié)構(gòu)(其父級(jí))
針對(duì) ModuleInjector
層次結(jié)構(gòu)(其父級(jí))
當(dāng)組件聲明依賴項(xiàng)時(shí),Angular 會(huì)嘗試使用它自己的 ElementInjector
來滿足該依賴。 如果組件的注入器缺少提供者,它將把請(qǐng)求傳給其父組件的 ElementInjector
。
這些請(qǐng)求將繼續(xù)轉(zhuǎn)發(fā),直到 Angular 找到可以處理該請(qǐng)求的注入器或用完祖先 ElementInjector
。
如果 Angular 在任何 ElementInjector
中都找不到提供者,它將返回到發(fā)起請(qǐng)求的元素,并在 ModuleInjector
層次結(jié)構(gòu)中進(jìn)行查找。如果 Angular 仍然找不到提供者,它將引發(fā)錯(cuò)誤。
如果你已在不同級(jí)別注冊(cè)了相同 DI
令牌的提供者,則 Angular 會(huì)用遇到的第一個(gè)來解析該依賴。例如,如果提供者已經(jīng)在需要此服務(wù)的組件中本地注冊(cè)了,則 Angular 不會(huì)再尋找同一服務(wù)的其它提供者。
可以使用 @Optional()
,@Self()
,@SkipSelf()
和 @Host()
來修飾 Angular 的解析行為。從 @angular/core
導(dǎo)入它們,并在注入服務(wù)時(shí)在組件類構(gòu)造函數(shù)中使用它們。
解析修飾符分為三類:
如果 Angular 找不到你要的東西該怎么辦,用 @Optional()
從哪里開始尋找,用 @SkipSelf()
到哪里停止尋找,用 @Host()
和 @Self()
默認(rèn)情況下,Angular 始終從當(dāng)前的 Injector
開始,并一直向上搜索。修飾符使你可以更改開始(默認(rèn)是自己)或結(jié)束位置。
另外,你可以組合除 @Host()
和 @Self()
之外的所有修飾符,當(dāng)然還有 @SkipSelf()
和 @Self()
。
@Optional()
允許 Angular 將你注入的服務(wù)視為可選服務(wù)。這樣,如果無(wú)法在運(yùn)行時(shí)解析它,Angular 只會(huì)將服務(wù)解析為 null
,而不會(huì)拋出錯(cuò)誤。在下面的示例中,服務(wù) OptionalService
沒有在 @NgModule()
或組件類中提供,所以它沒有在應(yīng)用中的任何地方。
Path:"resolution-modifiers/src/app/optional/optional.component.ts" 。
export class OptionalComponent {
constructor(@Optional() public optional?: OptionalService) {}
}
使用 @Self()
讓 Angular 僅查看當(dāng)前組件或指令的 ElementInjector
。
@Self()
的一個(gè)好例子是要注入某個(gè)服務(wù),但只有當(dāng)該服務(wù)在當(dāng)前宿主元素上可用時(shí)才行。為了避免這種情況下出錯(cuò),請(qǐng)將 @Self()
與 @Optional()
結(jié)合使用。
例如,在下面的 SelfComponent
中。請(qǐng)注意在構(gòu)造函數(shù)中注入的 LeafService
。
Path:"resolution-modifiers/src/app/self-no-data/self-no-data.component.ts" 。
@Component({
selector: 'app-self-no-data',
templateUrl: './self-no-data.component.html',
styleUrls: ['./self-no-data.component.css']
})
export class SelfNoDataComponent {
constructor(@Self() @Optional() public leaf?: LeafService) { }
}
在這個(gè)例子中,有一個(gè)父提供者,注入服務(wù)將返回該值,但是,使用 @Self()
和 @Optional()
注入的服務(wù)將返回 null
因?yàn)?@Self()
告訴注入器在當(dāng)前宿主元素上就要停止搜索。
另一個(gè)示例顯示了具有 FlowerService
提供者的組件類。在這個(gè)例子中,注入器沒有超出當(dāng)前 ElementInjector
就停止了,因?yàn)樗呀?jīng)找到了 FlowerService
并返回了黃色花朵????。
Path:"resolution-modifiers/src/app/self/self.component.ts" 。
@Component({
selector: 'app-self',
templateUrl: './self.component.html',
styleUrls: ['./self.component.css'],
providers: [{ provide: FlowerService, useValue: { emoji: '????' } }]
})
export class SelfComponent {
constructor(@Self() public flower: FlowerService) {}
}
@SkipSelf()
與 @Self()
相反。使用 @SkipSelf()
,Angular 在父 ElementInjector
中而不是當(dāng)前 ElementInjector
中開始搜索服務(wù)。因此,如果父 ElementInjector
對(duì) emoji
使用了值 ????(蕨類),但組件的 providers
數(shù)組中有 ????(楓葉),則 Angular 將忽略 ????(楓葉),而使用 ????(蕨類)。
要在代碼中看到這一點(diǎn),請(qǐng)先假定 emoji
的以下值就是父組件正在使用的值,如本服務(wù)所示:
Path:"resolution-modifiers/src/app/leaf.service.ts" 。
export class LeafService {
emoji = '????';
}
想象一下,在子組件中,你有一個(gè)不同的值 ????(楓葉),但你想使用父項(xiàng)的值。你就要使用 @SkipSelf()
:
Path:"resolution-modifiers/src/app/skipself/skipself.component.ts" 。
@Component({
selector: 'app-skipself',
templateUrl: './skipself.component.html',
styleUrls: ['./skipself.component.css'],
// Angular would ignore this LeafService instance
providers: [{ provide: LeafService, useValue: { emoji: '????' } }]
})
export class SkipselfComponent {
// Use @SkipSelf() in the constructor
constructor(@SkipSelf() public leaf: LeafService) { }
}
在這個(gè)例子中,你獲得的 emoji
值將為 ????(蕨類),而不是 ????(楓葉)。
如果值為 null
請(qǐng)同時(shí)使用 @SkipSelf()
和 @Optional()
來防止錯(cuò)誤。在下面的示例中,將 Person 服務(wù)注入到構(gòu)造函數(shù)中。@SkipSelf()
告訴 Angular 跳過當(dāng)前的注入器,如果 Person
服務(wù)為 null
,則 @Optional()
將防止報(bào)錯(cuò)。
class Person {
constructor(@Optional() @SkipSelf() parent?: Person) {}
}
@Host()
使你可以在搜索提供者時(shí)將當(dāng)前組件指定為注入器樹的最后一站。即使樹的更上級(jí)有一個(gè)服務(wù)實(shí)例,Angular 也不會(huì)繼續(xù)尋找。使用 @Host()
的例子如下:
Path:"resolution-modifiers/src/app/host/host.component.ts" 。
@Component({
selector: 'app-host',
templateUrl: './host.component.html',
styleUrls: ['./host.component.css'],
// provide the service
providers: [{ provide: FlowerService, useValue: { emoji: '????' } }]
})
export class HostComponent {
// use @Host() in the constructor when injecting the service
constructor(@Host() @Optional() public flower?: FlowerService) { }
}
由于 HostComponent
在其構(gòu)造函數(shù)中具有 @Host()
,因此,無(wú)論 HostComponent
的父級(jí)是否可能有 flower.emoji
值,該 HostComponent
都將使用 ????(黃色花朵)。
在組件類中提供服務(wù)時(shí),服務(wù)在 ElementInjector
樹中的可見性是取決于你在何處以及如何提供這些服務(wù)。
了解 Angular 模板的基礎(chǔ)邏輯結(jié)構(gòu)將為你配置服務(wù)并進(jìn)而控制其可見性奠定基礎(chǔ)。
組件在模板中使用,如以下示例所示:
<app-root>
<app-child></app-child>
</app-root>
注:
通常,你要在單獨(dú)的文件中聲明組件及其模板。為了理解注入系統(tǒng)的工作原理,從組合邏輯樹的視角來看它們是很有幫助的。使用術(shù)語(yǔ)“邏輯”將其與渲染樹(你的應(yīng)用程序 DOM 樹)區(qū)分開。為了標(biāo)記組件模板的位置,本指南使用
<#VIEW&
偽元素,該元素實(shí)際上不存在于渲染樹中,僅用于心智模型中。
下面是如何將 <app-root>
和 <app-child>
視圖樹組合為單個(gè)邏輯樹的示例:
<app-root>
<#VIEW>
<app-child>
<#VIEW>
...content goes here...
</#VIEW>
</app-child>
<#VIEW>
</app-root>
當(dāng)你在組件類中配置服務(wù)時(shí),了解這種 <#VIEW>
劃界的思想尤其重要。
你如何通過 @Component()
(或 @Directive()
)裝飾器提供服務(wù)決定了它們的可見性。以下各節(jié)演示了 providers
和 viewProviders
以及使用 @SkipSelf()
和 @Host()
修改服務(wù)可見性的方法。
組件類可以通過兩種方式提供服務(wù):
providers
數(shù)組 @Component({
...
providers: [
{provide: FlowerService, useValue: {emoji: '????'}}
]
})
viewProviders
數(shù)組 @Component({
...
viewProviders: [
{provide: AnimalService, useValue: {emoji: '????'}}
]
})
為了解 providers
和 viewProviders
對(duì)服務(wù)可見性的影響有何差異,以下各節(jié)將逐步構(gòu)建一個(gè)示例并在代碼和邏輯樹中比較 providers
和 viewProviders
的作用。
注:
在邏輯樹中,你會(huì)看到
@Provide
,@Inject
和@NgModule
,這些不是真正的 HTML 屬性,只是為了在這里證明其幕后的原理。
@Inject(Token)=&Value
表示,如果要將Token
注入邏輯樹中的此位置,則它的值為Value
。
@Provide(Token=Value)
表示,在邏輯樹中的此位置存在一個(gè)值為Value
的Token
提供者的聲明。
@NgModule(Token)
表示,應(yīng)在此位置使用后備的NgModule
注入器。
示例應(yīng)用程序的 root
提供了 FlowerService
,其 emoji 值為 ????(紅色芙蓉)。
Path:"providers-viewproviders/src/app/flower.service.ts" 。
@Injectable({
providedIn: 'root'
})
export class FlowerService {
emoji = '????';
}
考慮一個(gè)只有 AppComponent
和 ChildComponent
的簡(jiǎn)單應(yīng)用程序。最基本的渲染視圖看起來就像嵌套的 HTML 元素,例如:
<app-root> <!-- AppComponent selector -->
<app-child> <!-- ChildComponent selector -->
</app-child>
</app-root>
但是,在幕后,Angular 在解析注入請(qǐng)求時(shí)使用如下邏輯視圖表示形式:
<app-root> <!-- AppComponent selector -->
<#VIEW>
<app-child> <!-- ChildComponent selector -->
<#VIEW>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
此處的 <#VIEW>
表示模板的實(shí)例。請(qǐng)注意,每個(gè)組件都有自己的 <#VIEW>
。
了解此結(jié)構(gòu)可以告知你如何提供和注入服務(wù),并完全控制服務(wù)的可見性。
現(xiàn)在,考慮 <app-root>
只注入了 FlowerService
:
Path:"providers-viewproviders/src/app/app.component.ts" 。
export class AppComponent {
constructor(public flower: FlowerService) {}
}
將綁定添加到 <app-root>
模板來將結(jié)果可視化:
Path:"providers-viewproviders/src/app/app.component.html" 。
<p>Emoji from FlowerService: {{flower.emoji}}</p>
該視圖中的輸出為:
Emoji from FlowerService: ????
在邏輯樹中,這可以表示成如下形式:
<app-root @NgModule(AppModule)
@Inject(FlowerService) flower=>"????">
<#VIEW>
<p>Emoji from FlowerService: {{flower.emoji}} (????)</p>
<app-child>
<#VIEW>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
當(dāng) <app-root>
請(qǐng)求 FlowerService
時(shí),注入器的工作就是解析 FlowerService
令牌。令牌的解析分為兩個(gè)階段:
@NgModule()
委派該請(qǐng)求。在這個(gè)例子中,約束為:
<app-root>
的 <#VIEW>
開始,并結(jié)束于 <app-root>
。<app-root> @Component
的特殊之處在于它們還包括自己的 viewProviders
,這就是為什么搜索從 <app-root>
的 <#VIEW>
開始的原因。(對(duì)于匹配同一位置的指令,情況卻并非如此)。ElementInjector
中找不到注入令牌時(shí),就用 AppModule
充當(dāng)后備注入器。
現(xiàn)在,在 ChildComponent
類中,為 FlowerService
添加一個(gè)提供者,以便在接下來的小節(jié)中演示更復(fù)雜的解析規(guī)則:
Path:"providers-viewproviders/src/app/child.component.ts" 。
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
// use the providers array to provide a service
providers: [{ provide: FlowerService, useValue: { emoji: '????' } }]
})
export class ChildComponent {
// inject the service
constructor( public flower: FlowerService) { }
}
現(xiàn)在,在 @Component()
裝飾器中提供了 FlowerService
,當(dāng) <app-child>
請(qǐng)求該服務(wù)時(shí),注入器僅需要查找 <app-child>
自己的 ElementInjector
。不必再通過注入器樹繼續(xù)搜索。
下一步是將綁定添加到 ChildComponent
模板。
Path:"providers-viewproviders/src/app/child.component.html" 。
<p>Emoji from FlowerService: {{flower.emoji}}</p>
要渲染新的值,請(qǐng)?jiān)?AppComponent
模板的底部添加 <app-child>
,以便其視圖也顯示向日葵:
Child Component
Emoji from FlowerService: ????
在邏輯樹中,可以把它表示成這樣:
<app-root @NgModule(AppModule)
@Inject(FlowerService) flower=>"????">
<#VIEW>
<p>Emoji from FlowerService: {{flower.emoji}} (????)</p>
<app-child @Provide(FlowerService="????")
@Inject(FlowerService)=>"????"> <!-- search ends here -->
<#VIEW> <!-- search starts here -->
<h2>Parent Component</h2>
<p>Emoji from FlowerService: {{flower.emoji}} (????)</p>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
當(dāng) <app-child>
請(qǐng)求 FlowerService
時(shí),注入器從 <app-child>
的 <#VIEW>
開始搜索(包括 <#VIEW>
,因?yàn)樗菑?@Component()
注入的),并到 <app-child>
結(jié)束。在這個(gè)例子中,FlowerService
在 <app-child>
的 providers
數(shù)組中解析為向日葵????。注入器不必在注入器樹中進(jìn)一步查找。一旦找到 FlowerService
,它便停止運(yùn)行,再也看不到????(紅芙蓉)。
使用 viewProviders
數(shù)組是在 @Component()
裝飾器中提供服務(wù)的另一種方法。使用 viewProviders
使服務(wù)在 <#VIEW>
中可見。
除了使用
viewProviders
數(shù)組外,其它步驟與使用providers
數(shù)組相同。
該示例應(yīng)用程序具有第二個(gè)服務(wù) AnimalService
來演示 viewProviders
。
首先,創(chuàng)建一個(gè) AnimalService
與 emoji
的????(鯨魚)屬性:
Path:"providers-viewproviders/src/app/animal.service.ts" 。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AnimalService {
emoji = '????';
}
遵循與 FlowerService
相同的模式,將 AnimalService
注入 AppComponent
類:
Path:"providers-viewproviders/src/app/app.component.ts" 。
export class AppComponent {
constructor(public flower: FlowerService, public animal: AnimalService) {}
}
注:
你可以保留所有與
FlowerService
相關(guān)的代碼,因?yàn)樗梢耘cAnimalService
進(jìn)行比較。
添加一個(gè) viewProviders
數(shù)組,并將 AnimalService
也注入到 <app-child>
類中,但是給 emoji
一個(gè)不同的值。在這里,它的值為????(小狗)。
Path:"providers-viewproviders/src/app/child.component.ts" 。
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
// provide services
providers: [{ provide: FlowerService, useValue: { emoji: '????' } }],
viewProviders: [{ provide: AnimalService, useValue: { emoji: '????' } }]
})
export class ChildComponent {
// inject service
constructor( public flower: FlowerService, public animal: AnimalService) { }
}
將綁定添加到 ChildComponent
和 AppComponent
模板。在 ChildComponent
模板中,添加以下綁定:
Path:"providers-viewproviders/src/app/child.component.html" 。
<p>Emoji from AnimalService: {{animal.emoji}}</p>
此外,將其添加到 AppComponent
模板:
Path:"providers-viewproviders/src/app/app.component.html" 。
<p>Emoji from AnimalService: {{animal.emoji}}</p>
現(xiàn)在,你應(yīng)該在瀏覽器中看到兩個(gè)值:
AppComponent
Emoji from AnimalService: ????
Child Component
Emoji from AnimalService: ????
此 viewProviders
示例的邏輯樹如下:
<app-root @NgModule(AppModule)
@Inject(AnimalService) animal=>"????">
<#VIEW>
<app-child>
<#VIEW
@Provide(AnimalService="????")
@Inject(AnimalService=>"????")>
<!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
<p>Emoji from AnimalService: {{animal.emoji}} (????)</p>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
與 FlowerService
示例一樣,<app-child> @Component()
裝飾器中提供了 AnimalService
。這意味著,由于注入器首先在組件的 ElementInjector
中查找,因此它將找到 AnimalService
的值 ????(小狗)。它不需要繼續(xù)搜索 ElementInjector
樹,也不需要搜索ModuleInjector
。
為了看清 providers
和 viewProviders
的差異,請(qǐng)?jiān)谑纠刑砑恿硪粋€(gè)組件,并將其命名為 InspectorComponent
。InspectorComponent
將是 ChildComponent
的子 ChildComponent
。在 "inspector.component.ts" 中,將 FlowerService
和 AnimalService
注入構(gòu)造函數(shù)中:
Path:"providers-viewproviders/src/app/inspector/inspector.component.ts" 。
export class InspectorComponent {
constructor(public flower: FlowerService, public animal: AnimalService) { }
}
你不需要 providers
或 viewProviders
數(shù)組。接下來,在 "inspector.component.html" 中,從以前的組件中添加相同的 html:
Path:"providers-viewproviders/src/app/inspector/inspector.component.html" 。
<p>Emoji from FlowerService: {{flower.emoji}}</p>
<p>Emoji from AnimalService: {{animal.emoji}}</p>
別忘了將 InspectorComponent
添加到 AppModule declarations
數(shù)組。
Path:"providers-viewproviders/src/app/app.module.ts" 。
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, ChildComponent, InspectorComponent ],
bootstrap: [ AppComponent ],
providers: []
})
export class AppModule { }
接下來,確保你的 "child.component.html" 包含以下內(nèi)容:
Path:"providers-viewproviders/src/app/child/child.component.html" 。
<p>Emoji from FlowerService: {{flower.emoji}}</p>
<p>Emoji from AnimalService: {{animal.emoji}}</p>
<div class="container">
<h3>Content projection</h3>
<ng-content></ng-content>
</div>
<h3>Inside the view</h3>
<app-inspector></app-inspector>
前兩行帶有綁定,來自之前的步驟。新的部分是 <ng-content>
和 <app-inspector>
。<ng-content>
允許你投影內(nèi)容,ChildComponent
模板中的 <app-inspector>
使 InspectorComponent
成為 ChildComponent
的子組件。
接下來,將以下內(nèi)容添加到 "app.component.html" 中以利用內(nèi)容投影的優(yōu)勢(shì)。
Path:"providers-viewproviders/src/app/app.component.html" 。
<app-child><app-inspector></app-inspector></app-child>
現(xiàn)在,瀏覽器將渲染以下內(nèi)容,為簡(jiǎn)潔起見,省略了前面的示例:
//...Omitting previous examples. The following applies to this section.
Content projection: This is coming from content. Doesn't get to see
puppy because the puppy is declared inside the view only.
Emoji from FlowerService: ????
Emoji from AnimalService: ????
Emoji from FlowerService: ????
Emoji from AnimalService: ????
這四個(gè)綁定說明了 providers
和 viewProviders
之間的區(qū)別。由于????(小狗)在<#VIEW>
中聲明,因此投影內(nèi)容不可見。投影的內(nèi)容中會(huì)看到????(鯨魚)。
但是下一部分,InspectorComponent
是 ChildComponent
的子組件,InspectorComponent
在 <#VIEW>
內(nèi)部,因此當(dāng)它請(qǐng)求 AnimalService
時(shí),它會(huì)看到????(小狗)。
邏輯樹中的 AnimalService
如下所示:
<app-root @NgModule(AppModule)
@Inject(AnimalService) animal=>"????">
<#VIEW>
<app-child>
<#VIEW
@Provide(AnimalService="????")
@Inject(AnimalService=>"????")>
<!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
<p>Emoji from AnimalService: {{animal.emoji}} (????)</p>
<app-inspector>
<p>Emoji from AnimalService: {{animal.emoji}} (????)</p>
</app-inspector>
</#VIEW>
<app-inspector>
<#VIEW>
<p>Emoji from AnimalService: {{animal.emoji}} (????)</p>
</#VIEW>
</app-inspector>
</app-child>
</#VIEW>
</app-root>
<app-inspector>
的投影內(nèi)容中看到了????(鯨魚),而不是????(小狗),因?yàn)????(小狗)在 <app-child>
的 <#VIEW>
中。如果 <app-inspector>
也位于 <#VIEW>
則只能看到????(小狗)。
如何使用可見性修飾符 @Host()
,@Self()
和 @SkipSelf()
來限制 ElementInjector
的開始和結(jié)束范圍。
可見性裝飾器影響搜索注入令牌時(shí)在邏輯樹中開始和結(jié)束的位置。為此,要將可見性裝飾器放置在注入點(diǎn),即 constructor()
,而不是在聲明點(diǎn)。
為了修改該注入器從哪里開始尋找 FlowerService
,把 @SkipSelf()
加到 <app-child>
的 @Inject
聲明 FlowerService
中。該聲明在 <app-child>
構(gòu)造函數(shù)中,如 "child.component.ts" 所示:
constructor(@SkipSelf() public flower : FlowerService) { }
使用 @SkipSelf()
,<app-child>
注入器不會(huì)尋找自身來獲取 FlowerService
。相反,噴射器開始在 <app-root>
的 ElementInjector
中尋找 FlowerService
,在那里它什么也沒找到。 然后,它返回到 <app-child>
的 ModuleInjector
并找到????(紅芙蓉)值,這是可用的,因?yàn)?<app-child> ModuleInjector
和 <app-root> ModuleInjector
被展開成了一個(gè) ModuleInjector
。因此,UI 將渲染以下內(nèi)容:
Emoji from FlowerService: ????
在邏輯樹中,這種情況可能如下所示:
<app-root @NgModule(AppModule)
@Inject(FlowerService) flower=>"????">
<#VIEW>
<app-child @Provide(FlowerService="????")>
<#VIEW @Inject(FlowerService, SkipSelf)=>"????">
<!-- With SkipSelf, the injector looks to the next injector up the tree -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
盡管 <app-child>
提供了????(向日葵),但該應(yīng)用程序渲染了????(紅色芙蓉),因?yàn)?@SkipSelf()
導(dǎo)致當(dāng)前的注入器跳過了自身并尋找其父級(jí)。
如果現(xiàn)在將 @Host()
(以及 @SkipSelf()
)添加到了 FlowerService
的 @Inject
,其結(jié)果將為 null
。這是因?yàn)?@Host()
將搜索的上限限制為 <#VIEW>
。這是在邏輯樹中的情況:
<app-root @NgModule(AppModule)
@Inject(FlowerService) flower=>"????">
<#VIEW> <!-- end search here with null-->
<app-child @Provide(FlowerService="????")> <!-- start search here -->
<#VIEW @Inject(FlowerService, @SkipSelf, @Host, @Optional)=>null>
</#VIEW>
</app-parent>
</#VIEW>
</app-root>
在這里,服務(wù)及其值是相同的,但是 @Host()
阻止了注入器對(duì) FlowerService
進(jìn)行任何高于 <#VIEW>
的查找,因此找不到它并返回 null
。
該 <app-child>
目前提供在 viewProviders
數(shù)組中提供了值為 ????(小狗)的 AnimalService
。由于注入器只需要查看 <app-child>
的 ElementInjector
中的 AnimalService
,它就不會(huì)看到????(鯨魚)。
就像在 FlowerService
示例中一樣,如果將 @SkipSelf()
添加到 AnimalService
的構(gòu)造函數(shù)中,則注入器將不在 AnimalService
的當(dāng)前 <app-child>
的 ElementInjector
中查找 AnimalService
。
export class ChildComponent {
// add @SkipSelf()
constructor(@SkipSelf() public animal : AnimalService) { }
}
相反,注入器將從 <app-root> ElementInjector
開始找。請(qǐng)記住,<app-child>
類在 viewProviders
數(shù)組中 AnimalService
中提供了????(小狗)的值:
@Component({
selector: 'app-child',
...
viewProviders:
[{ provide: AnimalService, useValue: { emoji: '????' } }]
})
在 <app-child>
中使用 SkipSelf()
的邏輯樹是這樣的:
<app-root @NgModule(AppModule)
@Inject(AnimalService=>"????")>
<#VIEW><!-- search begins here -->
<app-child>
<#VIEW
@Provide(AnimalService="????")
@Inject(AnimalService, SkipSelf=>"????")>
<!--Add @SkipSelf -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
在 <app-child>
中使用 @SkipSelf()
,注入器就會(huì)在 <app-root>
的 ElementInjector
中找到 ????(鯨)。
如果把 @Host()
添加到 AnimalService
的構(gòu)造函數(shù)上,結(jié)果就是????(小狗),因?yàn)樽⑷肫鲿?huì)在 <app-child>
的 <#VIEW>
中查找 AnimalService
服務(wù)。這里是 <app-child>
類中的 viewProviders
數(shù)組和構(gòu)造函數(shù)中的 @Host()
:
@Component({
selector: 'app-child',
...
viewProviders:
[{ provide: AnimalService, useValue: { emoji: '????' } }]
})
export class ChildComponent {
constructor(@Host() public animal : AnimalService) { }
}
@Host()
導(dǎo)致注入器開始查找,直到遇到 <#VIEW>
的邊緣。
<app-root @NgModule(AppModule)
@Inject(AnimalService=>"????")>
<#VIEW>
<app-child>
<#VIEW
@Provide(AnimalService="????")
@Inject(AnimalService, @Host=>"????")> <!-- @Host stops search here -->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
將帶有第三個(gè)動(dòng)物????(刺猬)的 viewProviders
數(shù)組添加到 "app.component.ts" 的 @Component()
元數(shù)據(jù)中:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
viewProviders: [{ provide: AnimalService, useValue: { emoji: '????' } }]
})
接下來,同時(shí)把 @SkipSelf()
和 @Host()
加在 "child.component.ts" 中 AnimalService
的構(gòu)造函數(shù)中。這是 <app-child>
構(gòu)造函數(shù)中的 @Host()
和 @SkipSelf()
:
export class ChildComponent {
constructor(
@Host() @SkipSelf() public animal : AnimalService) { }
}
將 @Host()
和 SkipSelf()
應(yīng)用于 providers
數(shù)組中的 FlowerService
,結(jié)果為 null
,因?yàn)?@SkipSelf()
會(huì)在 <app-child>
的注入器中開始搜索,但是 @Host()
要求它在 <#VIEW>
停止搜索 —— 沒有 FlowerService
。在邏輯樹中,你可以看到 FlowerService
在 <app-child>
中可見,而在 <#VIEW>
中不可見。
不過,提供在 AppComponent
的 viewProviders
數(shù)組中的 AnimalService
,是可見的。
邏輯樹表示法說明了為何如此:
<app-root @NgModule(AppModule)
@Inject(AnimalService=>"????")>
<#VIEW @Provide(AnimalService="????")
@Inject(AnimalService, @SkipSelf, @Host, @Optional)=>"????">
<!-- ^^@SkipSelf() starts here, @Host() stops here^^ -->
<app-child>
<#VIEW @Provide(AnimalService="????")
@Inject(AnimalService, @SkipSelf, @Host, @Optional)=>"????">
<!-- Add @SkipSelf ^^-->
</#VIEW>
</app-child>
</#VIEW>
</app-root>
@SkipSelf()
導(dǎo)致注入器從 <app-root>
而不是 <app-child>
處開始對(duì) AnimalService
進(jìn)行搜索,而 @Host()
會(huì)在 <app-root>
的 <#VIEW>
處停止搜索。 由于 AnimalService
是通過 viewProviders
數(shù)組提供的,因此注入程序會(huì)在 <#VIEW>
找到????(刺猬)。
在不同級(jí)別配置一個(gè)或多個(gè)提供者的能力開辟了很有用的可能性。
出于架構(gòu)方面的考慮,可能會(huì)讓你決定把一個(gè)服務(wù)限制到只能在它所屬的那個(gè)應(yīng)用域中訪問。 比如,這個(gè)例子中包括一個(gè)用于顯示反派列表的 VillainsListComponent
,它會(huì)從 VillainsService
中獲得反派列表數(shù)據(jù)。
如果你在根模塊 AppModule
中(也就是你注冊(cè) HeroesService
的地方)提供 VillainsService
,就會(huì)讓應(yīng)用中的任何地方都能訪問到 VillainsService
,包括針對(duì)英雄的工作流。如果你稍后修改了 VillainsService
,就可能破壞了英雄組件中的某些地方。在根模塊 AppModule
中提供該服務(wù)將會(huì)引入此風(fēng)險(xiǎn)。
該怎么做呢?你可以在 VillainsListComponent
的 providers
元數(shù)據(jù)中提供 VillainsService
,就像這樣:
Path:"src/app/villains-list.component.ts (metadata)" 。
@Component({
selector: 'app-villains-list',
templateUrl: './villains-list.component.html',
providers: [ VillainsService ]
})
在 VillainsListComponent
的元數(shù)據(jù)中而不是其它地方提供 VillainsService
服務(wù),該服務(wù)就會(huì)只在 VillainsListComponent
及其子組件樹中可用。
VillainService
對(duì)于 VillainsListComponent
來說是單例的,因?yàn)樗褪窃谶@里聲明的。只要 VillainsListComponent
沒有銷毀,它就始終是 VillainService
的同一個(gè)實(shí)例。但是對(duì)于 VillainsListComponent
的多個(gè)實(shí)例,每個(gè) VillainsListComponent
的實(shí)例都會(huì)有自己的 VillainService
實(shí)例。
很多應(yīng)用允許用戶同時(shí)進(jìn)行多個(gè)任務(wù)。 比如,在納稅申報(bào)應(yīng)用中,申報(bào)人可以打開多個(gè)報(bào)稅單,隨時(shí)可能從一個(gè)切換到另一個(gè)。
本章要示范的場(chǎng)景仍然是基于《英雄指南》的。 想象一個(gè)外層的 HeroListComponent
,它顯示一個(gè)超級(jí)英雄的列表。
要打開一個(gè)英雄的報(bào)稅單,申報(bào)者點(diǎn)擊英雄名,它就會(huì)打開一個(gè)組件來編輯那個(gè)申報(bào)單。 每個(gè)選中的申報(bào)單都會(huì)在自己的組件中打開,并且可以同時(shí)打開多個(gè)申報(bào)單。
每個(gè)報(bào)稅單組件都有下列特征:
假設(shè) HeroTaxReturnComponent
還有一些管理并還原這些更改的邏輯。 這對(duì)于簡(jiǎn)單的報(bào)稅單來說是很容易的。 不過,在現(xiàn)實(shí)世界中,報(bào)稅單的數(shù)據(jù)模型非常復(fù)雜,對(duì)這些修改的管理可能不得不投機(jī)取巧。 你可以把這種管理任務(wù)委托給一個(gè)輔助服務(wù),就像這個(gè)例子中所做的。
報(bào)稅單服務(wù) HeroTaxReturnService
緩存了單條 HeroTaxReturn
,用于跟蹤那個(gè)申報(bào)單的變更,并且可以保存或還原它。 它還委托給了全應(yīng)用級(jí)的單例服務(wù) HeroService
,它是通過依賴注入機(jī)制取得的。
Path:"src/app/hero-tax-return.service.ts" 。
import { Injectable } from '@angular/core';
import { HeroTaxReturn } from './hero';
import { HeroesService } from './heroes.service';
@Injectable()
export class HeroTaxReturnService {
private currentTaxReturn: HeroTaxReturn;
private originalTaxReturn: HeroTaxReturn;
constructor(private heroService: HeroesService) { }
set taxReturn (htr: HeroTaxReturn) {
this.originalTaxReturn = htr;
this.currentTaxReturn = htr.clone();
}
get taxReturn (): HeroTaxReturn {
return this.currentTaxReturn;
}
restoreTaxReturn() {
this.taxReturn = this.originalTaxReturn;
}
saveTaxReturn() {
this.taxReturn = this.currentTaxReturn;
this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
}
}
下面是正在使用 HeroTaxReturnService
的 HeroTaxReturnComponent
組件。
Path:"src/app/hero-tax-return.component.ts" 。
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { HeroTaxReturn } from './hero';
import { HeroTaxReturnService } from './hero-tax-return.service';
@Component({
selector: 'app-hero-tax-return',
templateUrl: './hero-tax-return.component.html',
styleUrls: [ './hero-tax-return.component.css' ],
providers: [ HeroTaxReturnService ]
})
export class HeroTaxReturnComponent {
message = '';
@Output() close = new EventEmitter<void>();
get taxReturn(): HeroTaxReturn {
return this.heroTaxReturnService.taxReturn;
}
@Input()
set taxReturn (htr: HeroTaxReturn) {
this.heroTaxReturnService.taxReturn = htr;
}
constructor(private heroTaxReturnService: HeroTaxReturnService) { }
onCanceled() {
this.flashMessage('Canceled');
this.heroTaxReturnService.restoreTaxReturn();
};
onClose() { this.close.emit(); };
onSaved() {
this.flashMessage('Saved');
this.heroTaxReturnService.saveTaxReturn();
}
flashMessage(msg: string) {
this.message = msg;
setTimeout(() => this.message = '', 500);
}
}
通過 @Input()
屬性可以得到要編輯的報(bào)稅單,這個(gè)屬性被實(shí)現(xiàn)成了讀取器(getter
)和設(shè)置器(setter
)。 設(shè)置器根據(jù)傳進(jìn)來的報(bào)稅單初始化了組件自己的 HeroTaxReturnService
實(shí)例。 讀取器總是返回該服務(wù)所存英雄的當(dāng)前狀態(tài)。 組件也會(huì)請(qǐng)求該服務(wù)來保存或還原這個(gè)報(bào)稅單。
但如果該服務(wù)是一個(gè)全應(yīng)用范圍的單例就不行了。 每個(gè)組件就都會(huì)共享同一個(gè)服務(wù)實(shí)例,每個(gè)組件也都會(huì)覆蓋屬于其它英雄的報(bào)稅單。
要防止這一點(diǎn),就要在 HeroTaxReturnComponent
元數(shù)據(jù)的 providers
屬性中配置組件級(jí)的注入器,來提供該服務(wù)。
Path:"src/app/hero-tax-return.component.ts (providers)" 。
providers: [ HeroTaxReturnService ]
HeroTaxReturnComponent
有它自己的 HeroTaxReturnService
提供者。 回憶一下,每個(gè)組件的實(shí)例都有它自己的注入器。 在組件級(jí)提供服務(wù)可以確保組件的每個(gè)實(shí)例都得到一個(gè)自己的、私有的服務(wù)實(shí)例,而報(bào)稅單也不會(huì)再被意外覆蓋了。
在其它層級(jí)重新提供服務(wù)的另一個(gè)理由,是在組件樹的深層中把該服務(wù)替換為一個(gè)更專門化的實(shí)現(xiàn)。
考慮一個(gè)依賴于一系列服務(wù)的 Car
組件。 假設(shè)你在根注入器(代號(hào) A)中配置了通用的提供者:CarService、EngineService
和 TiresService
。
你創(chuàng)建了一個(gè)車輛組件(A),它顯示一個(gè)從另外三個(gè)通用服務(wù)構(gòu)造出的車輛。
然后,你創(chuàng)建一個(gè)子組件(B),它為 CarService
和 EngineService
定義了自己特有的提供者,它們具有適用于組件 B 的特有能力。
組件 B 是另一個(gè)組件 C 的父組件,而組件 C 又定義了自己的,更特殊的CarService
提供者。
在幕后,每個(gè)組件都有自己的注入器,這個(gè)注入器帶有為組件本身準(zhǔn)備的 0 個(gè)、1 個(gè)或多個(gè)提供者。
當(dāng)你在最深層的組件 C 解析 Car
的實(shí)例時(shí),它使用注入器 C 解析生成了一個(gè) Car
的實(shí)例,使用注入器 B 解析了 Engine
,而 Tires
則是由根注入器 A 解析的。
更多建議: