# 多级注入器
Angular 中的注入器有一些规则,你可以利用这些规则来在应用程序中获得所需的可注入对象可见性。通过了解这些规则,可以确定应在哪个 NgModule、组件或指令中声明服务提供者。
注意:本主题使用以下象形图。
HTML 实体 | 象形文字 |
---|---|
🌺 | 红芙蓉(🌺 ) |
🌻 | 向日葵 ( 🌻 )sunflower (🌻 ) |
🌼 | 黄色花(🌼 ) |
🌿 | 蕨类 ( 🌿 ) |
🍁 | 枫叶(🍁 ) |
🐳 | 鲸鱼 ( 🐳 ) |
🐶 | 狗 ( 🐶 ) |
🦔 | 刺猬 ( 🦔 ) |
你使用 Angular 构建的应用程序可能会变得非常大,管理这种复杂性的方法之一是将应用程序拆分为许多封装良好的小模块,这些模块本身也会拆分为定义明确的组件树。
你的页面中可能会有某些部分会与应用程序的其余部分完全独立地工作,它具有自己的服务本地副本和所需的其它依赖项。在应用程序中,这些部分使用的某些服务可能会与其它部分共享,或者与组件树中更深层的父组件共享,而其它依赖项则应该是私有的。
使用多级依赖注入,你可以隔离应用程序的各个部分,并允许它们保有自己的私有依赖项,不与应用程序的其余部分共享,或者让父组件仅与其子组件共享某些依赖项,而不与组件树的其余部分共享等。多级依赖注入能让你仅在需要时才在应用程序的不同部分之间共享依赖项。
# 注入器层次结构的类型
Angular 中的注入器有一些规则,你可以利用这些规则来在应用程序中获得所需的可注入对象的可见性。通过了解这些规则,可以确定应在哪个 NgModule、组件或指令中声明服务提供者。
Angular 中有两个注入器层次结构:
注入器层次结构 | 详情 |
---|---|
ModuleInjector 层次结构 | 使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector 。 |
ElementInjector 层次结构 | 在每个 DOM 元素上隐式创建。默认情况下,ElementInjector 是空的,除非你在 @Directive() 或 @Component() 的 providers 属性中配置它。 |
# ModuleInjector
可以通过以下两种方式之一配置 ModuleInjector
:
* 使用 @Injectable()
的 providedIn
属性引用 @NgModule()
或 root
* 使用 @NgModule()
的 providers
数组
Tree-shaking and @Injectable()
# 摇树优化与 @Injectable()
使用 @Injectable()
的 providedIn
属性优于 @NgModule()
的 providers
数组。使用 @Injectable()
的 providedIn
时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。
摇树优化对于库特别有用,因为使用该库的应用程序不需要注入它。在 服务与依赖注入简介
了解关于可摇树优化的提供者的更多信息。
ModuleInjector
由 @NgModule.providers
和 NgModule.imports
属性配置。ModuleInjector
是可以通过 NgModule.imports
递归找到的所有 providers 数组的扁平化。
子 ModuleInjector
是在惰性加载其它 @NgModules
时创建的。
使用 @Injectable()
的 providedIn
属性提供服务的方式如下:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // <--provides this service in the root ModuleInjector
})
export class ItemService {
name = 'telephone';
}
@Injectable()
装饰器标识服务类。该 providedIn
属性配置指定的 ModuleInjector
,这里的 root
会把让该服务在 root``ModuleInjector
上可用。
# 平台注入器
在 root
之上还有两个注入器,一个是额外的 ModuleInjector
,一个是 NullInjector()
。
思考下 Angular 要如何通过 main.ts
中的如下代码引导应用程序:
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {…})
bootstrapModule()
方法会创建一个由 AppModule
配置的注入器作为平台注入器的子注入器。也就是 root``ModuleInjector
。
platformBrowserDynamic()
方法创建一个由 PlatformModule
配置的注入器,该注入器包含特定平台的依赖项。这允许多个应用共享同一套平台配置。比如,无论你运行多少个应用程序,浏览器都只有一个 URL 栏。你可以使用 platformBrowser()
函数提供 extraProviders
,从而在平台级别配置特定平台的额外提供者。
层次结构中的下一个父注入器是 NullInjector()
,它是树的顶部。如果你在树中向上走了很远,以至于要在 NullInjector()
中寻找服务,那么除非使用 @Optional()
,否则将收到错误消息,因为最终所有东西都将以 NullInjector()
结束并返回错误,或者对于 @Optional()
,返回 null
。关于 @Optional()
的更多信息,请参阅本指南的 @Optional() 部分
。
下图展示了前面各段落描述的 root``ModuleInjector
及其父注入器之间的关系。
虽然 root
是一个特殊的别名,但其它 ModuleInjector
都没有别名。每当创建动态加载组件时,你还会创建 ModuleInjector
,比如路由器,它还会创建子 ModuleInjector
。
无论是使用 bootstrapModule()
的方法配置它,还是将所有提供者都用 root
注册到其自己的服务中,所有请求最终都会转发到 root
注入器。
@Injectable() vs. @NgModule()如果你在 AppModule
的 @NgModule()
中配置应用级提供者,它就会覆盖一个在 @Injectable()
的 root
元数据中配置的提供者。你可以用这种方式,来配置供多个应用共享的服务的非默认提供者。
下面的例子中,通过把 location 策略
的提供者添加到 AppModule
的 providers
列表中,为路由器配置了非默认的 location 策略
。
src/app/app.module.ts (providers)
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy }
]
# ElementInjector
Angular 会为每个 DOM 元素隐式创建 ElementInjector
。
可以用 @Component()
装饰器中的 providers
或 viewProviders
属性来配置 ElementInjector
以提供服务。比如,下面的 TestComponent
通过提供此服务来配置 ElementInjector
:
@Component({
…
providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent
注意:
参阅解析规则部分以了解 ModuleInjector
树和 ElementInjector
树之间的关系。
在组件中提供服务时,可以通过 ElementInjector
在该组件实例处使用该服务。根据解析规则部分描述的可见性规则,它也同样在子组件/指令处可见。
当组件实例被销毁时,该服务实例也将被销毁。
# @Directive()
和 @Component()
link
组件是一种特殊类型的指令,这意味着 @Directive()
具有 providers
属性,@Component()
也同样如此。 这意味着指令和组件都可以使用 providers
属性来配置提供者。当使用 providers
属性为组件或指令配置提供者时,该提供程商就属于该组件或指令的 ElementInjector
。同一元素上的组件和指令共享同一个注入器。
# 解析规则
当为组件/指令解析令牌时,Angular 分为两个阶段来解析它:
* 针对 ElementInjector
层次结构中它的父级。
* 针对 ModuleInjector
层次结构中它的父级。
当组件声明依赖项时,Angular 会尝试使用它自己的 ElementInjector
来满足该依赖。 如果组件的注入器缺少提供者,它将把请求传给其父组件的 ElementInjector
。
这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先 ElementInjector
。
如果 Angular 在任何 ElementInjector
中都找不到提供者,它将返回到发起请求的元素,并在 ModuleInjector
层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。
如果你已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖。比如,如果提供者已经在需要此服务的组件中本地注册了,则 Angular 不会再寻找同一服务的其它提供者。
# 解析修饰符
可以使用 @Optional()
,@Self()
,@SkipSelf()
和 @Host()
来修饰 Angular 的解析行为。从 @angular/core
导入它们,并在注入服务时在组件类构造函数中使用它们。
关于展示本节介绍的解析修饰符的可运行应用,请参阅解析修饰符范例解析修饰符范例/ 下载范例
。
# 修饰符的类型
解析修饰符分为三类:
* 如果 Angular 找不到你要的东西该怎么办,用 @Optional()
* 从哪里开始寻找,用 @SkipSelf()
* 到哪里停止寻找,用 @Host()
和 @Self()
默认情况下,Angular 始终从当前的 Injector
开始,并一直向上搜索。修饰符使你可以更改开始(默认是自己)或结束位置。
另外,你可以组合除 @Host()
和 @Self()
之外的所有修饰符,当然还有 @SkipSelf()
和 @Self()
。
# @Optional()
@Optional()
允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null
,而不会抛出错误。在下面的范例中,服务 OptionalService
没有在 @NgModule()
或组件类中提供,所以它没有在应用中的任何地方。
resolution-modifiers/src/app/optional/optional.component.tscontent_copyexport class OptionalComponent { constructor(@Optional() public optional?: OptionalService) {} }
# @Self()
使用 @Self()
让 Angular 仅查看当前组件或指令的 ElementInjector
。
@Self()
的一个好例子是要注入某个服务,但只有当该服务在当前宿主元素上可用时才行。为了避免这种情况下出错,请将 @Self()
与 @Optional()
结合使用。
比如,在下面的 SelfComponent
中。请注意在构造函数中注入的 LeafService
。
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) { }
}
在这个例子中,有一个父提供者,注入服务将返回该值,但是,使用 @Self()
和 @Optional()
注入的服务将返回 null
因为 @Self()
告诉注入器在当前宿主元素上就要停止搜索。
另一个范例显示了具有 FlowerService
提供者的组件类。在这个例子中,注入器没有超出当前 ElementInjector
就停止了,因为它已经找到了 FlowerService
并返回了黄色花朵🌼。
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()
@SkipSelf()
与 @Self()
相反。使用 @SkipSelf()
,Angular 在父 ElementInjector
中而不是当前 ElementInjector
中开始搜索服务。因此,如果父 ElementInjector
对 emoji
使用了值 🌿
(蕨类),但组件的 providers
数组中有 🍁
(枫叶),则 Angular 将忽略 🍁
(枫叶),而使用 🌿
(蕨类)。
要在代码中看到这一点,请先假定 emoji
的以下值就是父组件正在使用的值,如本服务所示:
resolution-modifiers/src/app/leaf.service.ts
export class LeafService {
emoji = '🌿';
}
想象一下,在子组件中,你有一个不同的值 🍁
(枫叶),但你想使用父项的值。你就要使用 @SkipSelf()
:
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) { }
}
在这个例子中,你获得的 emoji
值将为 🌿
(蕨类),而不是 🍁
(枫叶)。
# 合用 @SkipSelf()
和 @Optional()
如果值为 null
请同时使用 @SkipSelf()
和 @Optional()
来防止错误。在下面的范例中,将 Person
服务注入到构造函数中。@SkipSelf()
告诉 Angular 跳过当前的注入器,如果 Person
服务为 null
,则 @Optional()
将防止报错。
class Person {
constructor(@Optional() @SkipSelf() parent?: Person) {}
}
# @Host()
@Host()
使你可以在搜索提供者时将当前组件指定为注入器树的最后一站。即使树的更上级有一个服务实例,Angular 也不会继续寻找。使用 @Host()
的例子如下:
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
在其构造函数中具有 @Host()
,因此,无论 HostComponent
的父级是否可能有 flower.emoji
值,该 HostComponent
都将使用 🌼
(黄色花朵)。
# 模板的逻辑结构
在组件类中提供服务时,服务在 ElementInjector
树中的可见性是取决于你在何处以及如何提供这些服务。
了解 Angular 模板的基础逻辑结构将为你配置服务并进而控制其可见性奠定基础。
组件在模板中使用,如以下范例所示:
<app-root>
<app-child></app-child>
</app-root>
注意:
通常,你要在单独的文件中声明组件及其模板。为了理解注入系统的工作原理,从组合逻辑树的视角来看它们是很有帮助的。使用术语“逻辑”将其与渲染树(你的应用程序 DOM 树)区分开。为了标记组件模板的位置,本指南使用 <#VIEW>
伪元素,该元素实际上不存在于渲染树中,仅用于心智模型中。
下面是如何将 <app-root>
和 <app-child>
视图树组合为单个逻辑树的范例:
<app-root>
<#VIEW>
<app-child>
<#VIEW>
…content goes here…
</#VIEW>
</app-child>
</#VIEW>
</app-root>
当你在组件类中配置服务时,了解这种 <#VIEW>
划界的思想尤其重要。
# 在 @Component()
中提供服务
你如何通过 @Component()
(或 @Directive()
)装饰器提供服务决定了它们的可见性。以下各节演示了 providers
和 viewProviders
以及使用 @SkipSelf()
和 @Host()
修改服务可见性的方法。
组件类可以通过两种方式提供服务:
数组 | 详情 |
---|---|
使用 providers 数组 | content_copy@Component({ … providers: [ {provide: FlowerService, useValue: {emoji: '🌺'}} ] }) |
使用 viewProviders 数组 | content_copy@Component({ … viewProviders: [ {provide: AnimalService, useValue: {emoji: '🐶'}} ] }) |
为了解 providers
和 viewProviders
对服务可见性的影响有何差异,以下各节将逐步构建一个 现场演练
/ 下载范例
并在代码和逻辑树中比较 providers
和 viewProviders
的作用。
注意:
注意:在逻辑树中,你会看到 @Provide
,@Inject
和 @NgModule
,这些不是真正的 HTML 属性,只是为了在这里证明其幕后的原理。
ANGULAR 服务属性 | 详细信息 |
---|---|
@Inject(Token)=>Value | 演示如果 Token 在逻辑树中的此位置注入,其值将是 Value 。 |
@Provide(Token=Value) | 演示在逻辑树中的此位置有一个值为 Value 的 Token provider 声明。 |
@NgModule(Token) | 演示应该在此位置使用后备 NgModule 注入器。Demonstrates that a fallback NgModule injector should be used at this location. |
# 应用程序结构范例
范例应用程序的 root
提供了 FlowerService
,其 emoji
值为 🌺
(红色芙蓉)。
providers-viewproviders/src/app/flower.service.ts
@Injectable({
providedIn: 'root'
})
export class FlowerService {
emoji = '🌺';
}
考虑一个只有 AppComponent
和 ChildComponent
的简单应用程序。最基本的渲染视图看起来就像嵌套的 HTML 元素,比如:
<app-root> <!-- AppComponent selector -->
<app-child> <!-- ChildComponent selector -->
</app-child>
</app-root>
但是,在幕后,Angular 在解析注入请求时使用如下逻辑视图表示形式:
<app-root> <!-- AppComponent selector -->
<#VIEW>
<app-child> <!-- ChildComponent selector -->
<#VIEW>
</#VIEW>
</app-child>
</#VIEW>
</app-root>
此处的 <#VIEW>
表示模板的实例。请注意,每个组件都有自己的 <#VIEW>
。
了解此结构可以告知你如何提供和注入服务,并完全控制服务的可见性。
现在,考虑 <app-root>
只注入了 FlowerService
:
providers-viewproviders/src/app/app.component.ts
export class AppComponent {
constructor(public flower: FlowerService) {}
}
将绑定添加到 <app-root>
模板来将结果可视化:
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>
当 <app-root>
请求 FlowerService
时,注入器的工作就是解析 FlowerService
令牌。令牌的解析分为两个阶段:
* 注入器确定逻辑树中搜索的开始位置和结束位置。注入程序从起始位置开始,并在逻辑树的每个级别上查找令牌。如果找到令牌,则将其返回。
* 如果未找到令牌,则注入程序将寻找最接近的父 @NgModule()
委派该请求。
在这个例子中,约束为:
* 从属于 <app-root>
的 <#VIEW>
开始,并结束于 <app-root>
。
* 通常,搜索的起点就是注入点。但是,在这个例子中,<app-root>``@Component
的特殊之处在于它们还包括自己的 viewProviders
,这就是为什么搜索从 <app-root>
的 <#VIEW>
开始的原因。对于匹配同一位置的指令,情况却并非如此。
* 结束位置恰好与组件本身相同,因为它就是此应用程序中最顶层的组件。
* 当在 ElementInjector
中找不到注入令牌时,就用 AppModule
充当后备注入器。
# 使用 providers
数组
现在,在 ChildComponent
类中,为 FlowerService
添加一个提供者,以便在接下来的小节中演示更复杂的解析规则:
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) { }
}
现在,在 @Component()
装饰器中提供了 FlowerService
,当 <app-child>
请求该服务时,注入器仅需要查找 <app-child>
中的 ElementInjector
。不必再通过注入器树继续搜索。
下一步是将绑定添加到 ChildComponent
模板。
providers-viewproviders/src/app/child.component.html
<p>Emoji from FlowerService: {{flower.emoji}}</p>
要渲染新的值,请在 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>
当 <app-child>
请求 FlowerService
时,注入器从 <app-child>
的 <#VIEW>
开始搜索(包括 <#VIEW>
,因为它是从 @Component()
注入的),并到 <app-child>
结束。在这个例子中,FlowerService
在 <app-child>
的 providers
数组中解析为向日葵🌻。注入器不必在注入器树中进一步查找。一旦找到 FlowerService
,它便停止运行,再也看不到🌺(红芙蓉)。
# 使用 viewProviders
数组
使用 viewProviders
数组是在 @Component()
装饰器中提供服务的另一种方法。使用 viewProviders
使服务在 <#VIEW>
中可见。
除了使用 viewProviders
数组外,其它步骤与使用 providers
数组相同。
关于这些步骤的说明,请继续本节。如果你可以自行设置,请跳至修改服务可用性一节。
该范例应用程序具有第二个服务 AnimalService
来演示 viewProviders
。
首先,创建一个 AnimalService
与 emoji
的🐳(鲸鱼)属性:
providers-viewproviders/src/app/animal.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AnimalService {
emoji = '🐳';
}
遵循与 FlowerService
相同的模式,将 AnimalService
注入 AppComponent
类:
providers-viewproviders/src/app/app.component.ts
export class AppComponent {
constructor(public flower: FlowerService, public animal: AnimalService) {}
}
TIP
你可以保留所有与 FlowerService
相关的代码,因为它可以与 AnimalService
进行比较。
添加一个 viewProviders
数组,并将 AnimalService
也注入到 <app-child>
类中,但是给 emoji
一个不同的值。在这里,它的值为🐶(小狗)。
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
模板中,添加以下绑定:
providers-viewproviders/src/app/child.component.html
<p>Emoji from AnimalService: {{animal.emoji}}</p>
此外,将其添加到 AppComponent
模板:
providers-viewproviders/src/app/app.component.html
<p>Emoji from AnimalService: {{animal.emoji}}</p>
现在,你应该在浏览器中看到两个值:
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
的值 🐶(小狗)。它不需要继续搜索 ElementInjector
树,也不需要搜索 ModuleInjector
。
# providers
与 viewProviders
为了看清 providers
和 viewProviders
的差异,请在范例中添加另一个组件,并将其命名为 InspectorComponent
。InspectorComponent
将是 ChildComponent
的子 ChildComponent
。在 inspector.component.ts
中,将 FlowerService
和 AnimalService
注入构造函数中:
providers-viewproviders/src/app/inspector/inspector.component.ts
export class InspectorComponent {
constructor(public flower: FlowerService, public animal: AnimalService) { }
}
你不需要 providers
或 viewProviders
数组。接下来,在 inspector.component.html
中,从以前的组件中添加相同的 html:
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
数组。
providers-viewproviders/src/app/app.module.ts
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, ChildComponent, InspectorComponent ],
bootstrap: [ AppComponent ],
providers: []
})
export class AppModule { }
接下来,确保你的 child.component.html
包含以下内容:
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>
允许你投影内容,ChildComponent
模板中的 <app-inspector>
使 InspectorComponent
成为 ChildComponent
的子组件。
接下来,将以下内容添加到 app.component.html
中以利用内容投影的优势。
providers-viewproviders/src/app/app.component.html
<app-child><app-inspector></app-inspector></app-child>
现在,浏览器将渲染以下内容,为简洁起见,省略了前面的范例:
//…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: 🐶
这四个绑定说明了 providers
和 viewProviders
之间的区别。由于🐶(小狗)在 <#VIEW>
中声明,因此投影内容不可见。投影的内容中会看到🐳(鲸鱼)。
但是下一部分,InspectorComponent
是 ChildComponent
的子组件,InspectorComponent
在 <#VIEW>
内部,因此当它请求 AnimalService
时,它会看到🐶(小狗)。
逻辑树中的 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>
<div class="container">
<h3>Content projection</h3>
<app-inspector @Inject(AnimalService) animal=>"🐳">
<p>Emoji from AnimalService: {{animal.emoji}} (🐳)</p>
</app-inspector>
</div>
</#VIEW>
<app-inspector>
<#VIEW>
<p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
</#VIEW>
</app-inspector>
</app-child>
</#VIEW>
</app-root>
<app-inspector>
的投影内容中看到了🐳(鲸鱼),而不是🐶(小狗),因为🐶(小狗)在 <app-child>
的 <#VIEW>
中。如果 <app-inspector>
也位于 <#VIEW>
则只能看到🐶(小狗)。
# 修改服务可见性
本节讲的是如何使用可见性修饰符 @Host()
,@Self()
和 @SkipSelf()
来限制 ElementInjector
的开始和结束范围。
# 提供者令牌的可见性
可见性装饰器影响搜索注入令牌时在逻辑树中开始和结束的位置。为此,要将可见性装饰器放置在注入点,即 constructor()
,而不是在声明点。
为了修改该注入器从哪里开始寻找 FlowerService
,把 @SkipSelf()
加到 <app-child>
的 @Inject
声明 FlowerService
中。该声明在 <app-child>
构造函数中,如 child.component.ts
所示:
constructor(@SkipSelf() public flower : FlowerService) { }
使用 @SkipSelf()
,<app-child>
注入器不会寻找自身来获取 FlowerService
。相反,注入器开始在 <app-root>
的 ElementInjector
中寻找 FlowerService
,在那里它什么也没找到。 然后,它返回到 <app-child>
的 ModuleInjector
并找到🌺(红芙蓉)值,这是可用的,因为 <app-child>``ModuleInjector
和 <app-root>``ModuleInjector
被展开成了一个 ModuleInjector
。因此,UI 将渲染以下内容:
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>
提供了🌻(向日葵),但该应用程序渲染了🌺(红色芙蓉),因为 @SkipSelf()
导致当前的注入器跳过了自身并寻找其父级。
如果现在将 @Host()
(以及 @SkipSelf()
)添加到了 FlowerService
的 @Inject
,其结果将为 null
。这是因为 @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>
在这里,服务及其值是相同的,但是 @Host()
阻止了注入器对 FlowerService
进行任何高于 <#VIEW>
的查找,因此找不到它并返回 null
。
TIP
范例应用程序使用 @Optional()
因此该应用程序不会引发错误,但是其原理是一样的。
# @SkipSelf()
和 viewProviders
该 <app-child>
目前提供在 viewProviders
数组中提供了值为 🐶(小狗)的 AnimalService
。由于注入器只需要查看 <app-child>
的 ElementInjector
中的 AnimalService
,它就不会看到🐳(鲸鱼)。
在 FlowerService
这个例子中,如果将 @SkipSelf()
添加到 AnimalService
的构造函数中,则注入器将不在 AnimalService
的当前 <app-child>
的 ElementInjector
中查找 AnimalService
。
export class ChildComponent {
// add @SkipSelf()
constructor(@SkipSelf() public animal : AnimalService) { }
}
相反,注入器将从 <app-root>``ElementInjector
开始找。请记住,<app-child>
类在 viewProviders
数组中 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()
,注入器就会在 <app-root>
的 ElementInjector
中找到 🐳(鲸)。
# @Host()
和 viewProviders
如果把 @Host()
添加到 AnimalService
的构造函数上,结果就是🐶(小狗),因为注入器会在 <app-child>
的 <#VIEW>
中查找 AnimalService
服务。这里是 <app-child>
类中的 viewProviders
数组和构造函数中的 @Host()
:
@Component({
selector: 'app-child',
…
viewProviders:
[{ provide: AnimalService, useValue: { emoji: '🐶' } }]
})
export class ChildComponent {
constructor(@Host() public animal : AnimalService) { }
}
@Host()
导致注入器开始查找,直到遇到 <#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>
将带有第三个动物🦔(刺猬)的 viewProviders
数组添加到 app.component.ts
的 @Component()
元数据中:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
viewProviders: [{ provide: AnimalService, useValue: { emoji: '🦔' } }]
})
接下来,同时把 @SkipSelf()
和 @Host()
加在 child.component.ts
中 AnimalService
的构造函数中。这是 <app-child>
构造函数中的 @Host()
和 @SkipSelf()
:
export class ChildComponent {
constructor(
@Host() @SkipSelf() public animal : AnimalService) { }
}
将 @Host()
和 SkipSelf()
应用于 providers
数组中的 FlowerService
,结果为 null
,因为 @SkipSelf()
会在 <app-child>
的注入器中开始搜索,但是 @Host()
要求它在 <#VIEW>
停止搜索 —— 没有 FlowerService
。在逻辑树中,你可以看到 FlowerService
在 <app-child>
中可见,而在 <#VIEW>
中不可见。
不过,提供在 AppComponent
的 viewProviders
数组中的 AnimalService
,是可见的。
逻辑树表示法说明了为何如此:
<app-root @NgModule(AppModule)
@Inject(AnimalService=>"🐳")>
<#VIEW @Provide(AnimalService="🦔")
@Inject(AnimalService, @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()
导致注入器从 <app-root>
而不是 <app-child>
处开始对 AnimalService
进行搜索,而 @Host()
会在 <app-root>
的 <#VIEW>
处停止搜索。 由于 AnimalService
是通过 viewProviders
数组提供的,因此注入程序会在 <#VIEW>
找到🦔(刺猬)。
# ElementInjector
用例范例
在不同级别配置一个或多个提供者的能力开辟了很有用的可能性。要查看正在运行的应用中的以下情况,请参阅英雄范例英雄范例/ 下载范例
。
# 场景:服务隔离
出于架构方面的考虑,可能会让你决定把一个服务限制到只能在它所属的那个应用域中访问。 比如,这个例子中包括一个用于显示反派列表的 VillainsListComponent
,它会从 VillainsService
中获得反派列表数据。
如果你在根模块 AppModule
中(也就是你注册 HeroesService
的地方)提供 VillainsService
,就会让应用中的任何地方都能访问到 VillainsService
,包括针对英雄的工作流。如果你稍后修改了 VillainsService
,就可能破坏了英雄组件中的某些地方。在根模块 AppModule
中提供该服务将会引入此风险。
该怎么做呢?你可以在 VillainsListComponent
的 providers
元数据中提供 VillainsService
,就像这样:
src/app/villains-list.component.ts (metadata)
@Component({
selector: 'app-villains-list',
templateUrl: './villains-list.component.html',
providers: [ VillainsService ]
})
在 VillainsListComponent
的元数据中而不是其它地方提供 VillainsService
服务,该服务就会只在 VillainsListComponent
及其子组件树中可用。
VillainService
对于 VillainsListComponent
来说是单例的,因为它就是在这里声明的。只要 VillainsListComponent
没有销毁,它就始终是 VillainService
的同一个实例。但是对于 VillainsListComponent
的多个实例,每个 VillainsListComponent
的实例都会有自己的 VillainService
实例。
# 场景:多重编辑会话
很多应用允许用户同时进行多个任务。 比如,在纳税申报应用中,申报人可以打开多个报税单,随时可能从一个切换到另一个。
为了演示这种场景,假设有一个显示超级英雄列表的外部 HeroListComponent
。
要打开一个英雄的报税单,申报者点击英雄名,它就会打开一个组件来编辑那个申报单。 每个选中的申报单都会在自己的组件中打开,并且可以同时打开多个申报单。
每个报税单组件都有下列特征:
* 属于它自己的报税单会话
* 可以修改一个报税单,而不会影响另一个组件中的申报单
* 能把所做的修改保存到它的报税单中,或者放弃它们
假设 HeroTaxReturnComponent
还有一些管理并还原这些更改的逻辑。 这对于简单的报税单来说是很容易的。 不过,在现实世界中,报税单的数据模型非常复杂,对这些修改的管理可能不得不投机取巧。 你可以把这种管理任务委托给一个辅助服务,就像这个例子中所做的。
报税单服务 HeroTaxReturnService 缓存了单条 HeroTaxReturn,用于跟踪那个申报单的变更,并且可以保存或还原它。 它把后两项职责委托给了全应用级的单例服务 HeroService,HeroService 是通过依赖注入机制取得的。
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
组件。
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()
属性可以得到要编辑的报税单,这个属性被实现成了读取器(getter)和设置器(setter)。 设置器根据传进来的报税单初始化了组件自己的 HeroTaxReturnService
实例。 读取器总是返回该服务所存英雄的当前状态。 组件也会请求该服务来保存或还原这个报税单。
但如果该服务是一个全应用范围的单例就不行了。 每个组件就都会共享同一个服务实例,每个组件也都会覆盖属于其它英雄的报税单。
要防止这一点,就要在 HeroTaxReturnComponent
元数据的 providers
属性中配置组件级的注入器,来提供该服务。
src/app/hero-tax-return.component.ts (providers)
providers: [ HeroTaxReturnService ]
HeroTaxReturnComponent
有它自己的 HeroTaxReturnService
提供者。 回忆一下,每个组件的实例都有它自己的注入器。 在组件级提供服务可以确保组件的每个实例都得到一个自己的、私有的服务实例。这可以确保报税单不会被意外覆盖。
该场景代码中的其它部分依赖另一些 Angular 的特性和技术,你将会在本文档的其它章节学到。 你可以到现场演练/ 下载范例
查看代码和下载它。
# 场景:专门的提供者
在其它层级重新提供服务的另一个理由,是在组件树的深层中把该服务替换为一个更专门化的实现。
例如,考虑一个包含轮胎服务信息并依赖其它服务来提供有关汽车的更多详细信息的 Car
组件。
标记为 (A) 的根注入器使用通用提供者来获取有关 CarService
和 EngineService
的详细信息。
* Car
组件 (A)。组件 (A) 显示有关汽车的轮胎服务数据,并指定通用服务以提供有关汽车的更多信息。
* 子组件 (B)。组件 (B) 为 CarService
和 EngineService
定义了自己的特化的提供者,它们具有适合组件 (B) 中发生的事情的特殊能力。
* 子组件 (C) 作为组件 (B) 的子组件。组件 (C) 为 CarService
定义了自己的、更加特化的提供者。
在幕后,每个组件都有自己的注入器,这个注入器带有为组件本身准备的 0 个、1 个或多个提供者。
当你在最深的组件 (C) 处解析 Car
实例时,其注入器会生成:
* 由注入器 (C) 解析的 Car
实例
* 由注入器 (B) 解析的 Engine
* 它的 Tires
由根注入器 (A) 解析。
# 关于依赖注入的更多知识
要了解关于 Angular 依赖注入的更多信息,参阅 DI 提供者
和 DI 实战
两章。