Angular9 多級(jí)注入器

2020-07-03 15:49 更新

Angular 中的注入器有一些規(guī)則,你可以利用這些規(guī)則來在應(yīng)用程序中獲得所需的可注入對(duì)象可見性。通過了解這些規(guī)則,可以確定應(yīng)在哪個(gè) NgModule、組件或指令中聲明服務(wù)提供者。

兩個(gè)注入器層次結(jié)構(gòu)

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

可以通過以下兩種方式之一配置 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.providersNgModule.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 上可用。

平臺(tái)注入器

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 注入器。

@Injectable() vs. @NgModule()

如果你在 AppModule@NgModule() 中配置應(yīng)用級(jí)提供者,它就會(huì)覆蓋一個(gè)在 @Injectable()root 元數(shù)據(jù)中配置的提供者。你可以用這種方式,來配置供多個(gè)應(yīng)用共享的服務(wù)的非默認(rèn)提供者。

下面的例子中,通過把 location 策略 的提供者添加到 AppModuleproviders 列表中,為路由器配置了非默認(rèn)的 location 策略。

Path:"src/app/app.module.ts (providers)" 。

providers: [
  { provide: LocationStrategy, useClass: HashLocationStrategy }
]

ElementInjector

Angular 會(huì)為每個(gè) DOM 元素隱式創(chuàng)建 ElementInjector。

可以用 @Component() 裝飾器中的 providersviewProviders 屬性來配置 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() 和 @Component()

組件是一種特殊類型的指令,這意味著 @Directive() 具有 providers 屬性,@Component() 也同樣如此。 這意味著指令和組件都可以使用 providers 屬性來配置提供者。當(dāng)使用 providers 屬性為組件或指令配置提供者時(shí),該提供程商就屬于該組件或指令的 ElementInjector。同一元素上的組件和指令共享同一個(gè)注入器。

解析規(guī)則

當(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()

@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()

使用 @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()

@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 值將為 ????(蕨類),而不是 ????(楓葉)。

@SkipSelf() with @Optional()

如果值為 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()

@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 都將使用 ????(黃色花朵)。

模板的邏輯結(jié)構(gòu)

在組件類中提供服務(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() 中提供服務(wù)

你如何通過 @Component() (或 @Directive() )裝飾器提供服務(wù)決定了它們的可見性。以下各節(jié)演示了 providersviewProviders 以及使用 @SkipSelf()@Host() 修改服務(wù)可見性的方法。

組件類可以通過兩種方式提供服務(wù):

  1. 使用 providers 數(shù)組

    @Component({
      ...
      providers: [
        {provide: FlowerService, useValue: {emoji: '????'}}
      ]
    })

  1. 使用 viewProviders 數(shù)組

    @Component({
      ...
      viewProviders: [
        {provide: AnimalService, useValue: {emoji: '????'}}
      ]
    })

為了解 providersviewProviders 對(duì)服務(wù)可見性的影響有何差異,以下各節(jié)將逐步構(gòu)建一個(gè)示例并在代碼和邏輯樹中比較 providersviewProviders 的作用。

注:

在邏輯樹中,你會(huì)看到 @Provide,@Inject@NgModule,這些不是真正的 HTML 屬性,只是為了在這里證明其幕后的原理。

  • @Inject(Token)=&Value 表示,如果要將 Token 注入邏輯樹中的此位置,則它的值為 Value。

  • @Provide(Token=Value) 表示,在邏輯樹中的此位置存在一個(gè)值為 ValueToken 提供者的聲明。

  • @NgModule(Token) 表示,應(yīng)在此位置使用后備的 NgModule 注入器。

應(yīng)用程序結(jié)構(gòu)示例

示例應(yīng)用程序的 root 提供了 FlowerService,其 emoji 值為 ????(紅色芙蓉)。

Path:"providers-viewproviders/src/app/flower.service.ts" 。

@Injectable({
  providedIn: 'root'
})
export class FlowerService {
  emoji = '????';
}

考慮一個(gè)只有 AppComponentChildComponent 的簡(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è)階段:

  1. 注入器確定邏輯樹中搜索的開始位置和結(jié)束位置。注入程序從起始位置開始,并在邏輯樹的每個(gè)級(jí)別上查找令牌。如果找到令牌,則將其返回。

  1. 如果未找到令牌,則注入程序?qū)ふ易罱咏母?@NgModule() 委派該請(qǐng)求。

在這個(gè)例子中,約束為:

  1. 從屬于 <app-root><#VIEW> 開始,并結(jié)束于 <app-root>

  • 通常,搜索的起點(diǎn)就是注入點(diǎn)。但是,在這個(gè)例子中,<app-root> @Component 的特殊之處在于它們還包括自己的 viewProviders,這就是為什么搜索從 <app-root><#VIEW> 開始的原因。(對(duì)于匹配同一位置的指令,情況卻并非如此)。

  • 結(jié)束位置恰好與組件本身相同,因?yàn)樗褪谴藨?yīng)用程序中最頂層的組件。

  1. 當(dāng)在 ElementInjector 中找不到注入令牌時(shí),就用 AppModule 充當(dāng)后備注入器。

使用 providers 數(shù)組

現(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ù)組

使用 viewProviders 數(shù)組是在 @Component() 裝飾器中提供服務(wù)的另一種方法。使用 viewProviders 使服務(wù)在 <#VIEW> 中可見。

除了使用 viewProviders 數(shù)組外,其它步驟與使用 providers 數(shù)組相同。

該示例應(yīng)用程序具有第二個(gè)服務(wù) AnimalService 來演示 viewProviders。

首先,創(chuàng)建一個(gè) AnimalServiceemoji 的????(鯨魚)屬性:

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)樗梢耘c AnimalService 進(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) { }
}

將綁定添加到 ChildComponentAppComponent 模板。在 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

為了看清 providersviewProviders 的差異,請(qǐng)?jiān)谑纠刑砑恿硪粋€(gè)組件,并將其命名為 InspectorComponent。InspectorComponent 將是 ChildComponent 的子 ChildComponent。在 "inspector.component.ts" 中,將 FlowerServiceAnimalService 注入構(gòu)造函數(shù)中:

Path:"providers-viewproviders/src/app/inspector/inspector.component.ts" 。

export class InspectorComponent {
  constructor(public flower: FlowerService, public animal: AnimalService) { }
}

你不需要 providersviewProviders 數(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è)綁定說明了 providersviewProviders 之間的區(qū)別。由于????(小狗)在<#VIEW>中聲明,因此投影內(nèi)容不可見。投影的內(nèi)容中會(huì)看到????(鯨魚)。

但是下一部分,InspectorComponentChildComponent 的子組件,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> 則只能看到????(小狗)。

修改服務(wù)可見性

如何使用可見性修飾符 @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。

@SkipSelf() 和 viewProviders

<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() 和 viewProviders

如果把 @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> 中不可見。

不過,提供在 AppComponentviewProviders 數(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> 找到????(刺猬)。

ElementInjector 用例示例

在不同級(jí)別配置一個(gè)或多個(gè)提供者的能力開辟了很有用的可能性。

場(chǎng)景:服務(wù)隔離

出于架構(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)。

該怎么做呢?你可以在 VillainsListComponentproviders 元數(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í)例。

場(chǎng)景:多重編輯會(huì)話

很多應(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)稅單組件都有下列特征:

  • 屬于它自己的報(bào)稅單會(huì)話。

  • 可以修改一個(gè)報(bào)稅單,而不會(huì)影響另一個(gè)組件中的申報(bào)單。

  • 能把所做的修改保存到它的報(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();
  }
}

下面是正在使用 HeroTaxReturnServiceHeroTaxReturnComponent 組件。

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ì)再被意外覆蓋了。

場(chǎng)景:專門的提供者

在其它層級(jí)重新提供服務(wù)的另一個(gè)理由,是在組件樹的深層中把該服務(wù)替換為一個(gè)更專門化的實(shí)現(xiàn)。

考慮一個(gè)依賴于一系列服務(wù)的 Car 組件。 假設(shè)你在根注入器(代號(hào) A)中配置了通用的提供者:CarService、EngineServiceTiresService。

你創(chuàng)建了一個(gè)車輛組件(A),它顯示一個(gè)從另外三個(gè)通用服務(wù)構(gòu)造出的車輛。

然后,你創(chuàng)建一個(gè)子組件(B),它為 CarServiceEngineService 定義了自己特有的提供者,它們具有適用于組件 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 解析的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)