Angular复兴
Angular

层级注入器

Guo Eagle
#Angular

通过依赖注入的概念我们知道,创建实例的工作都交给Ioc 容器(也叫注入器)了,通过构造函数参数装饰器@Inject(DIToken)告诉注入器我们需要注入DIToken对应的依赖值,注入器就会帮我们查找依赖并返回值,Angular 应用启动会默认创建相关的注入器,而且 Angular 的注入器是有层级的,类似于一个 Dom 树。

Angular 中依赖注入提供的依赖值都是单例的,为了让不同的模块或者组件可以注入不同的实例,只要让两个组件的有独立的注入器即可,每个注入器都提供同一个依赖,这样就可以实现非全局单例的效果,有了层级注入器后,可以通过层级一层一层往上 解析 的规则,可以实现更高级的功能。

两个注入器层级

Angular 中有两个注入器层次结构:

ModuleInjector

可以通过以下两种方式之一配置ModuleInjector

摇树优化与 @Injectable(),使用@Injectable()providedIn属性优于@NgModule()providers数组,因为使用@Injectable()providedIn时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。 摇树优化对于库特别有用,因为使用该库的应用程序不需要注入它。在 服务与依赖注入简介了解关于可摇树优化的提供者的更多信息。

需要特别注意:

第一点的意思就是AppModule导入了FeatureAModuleFeatureBModule,那么 Angular 会根据模块树找到所有模块配置的providers并打平存放到一起,这就意味着整个应用程序所有地方都可以注入这些打平的providers,这就是 Angular 模块下的服务与组件/指令/管道所不同的地方,组件/指令/管道在某个模块定义,只要没有导出,其他模块都无法使用,必须导出才可以被导入该模块的组件使用,其次就是当有重复 DI Token 提供的依赖时,后提供的会覆盖之前提供的。

第二点可以不严谨的认为整个应用程序只有一个根模块注入器,只有通过路由的懒加载才会创建子模块注入器,因为当懒加载模块 FeatureCModule还没有被加载时,AppModule并不能通过模块树找到FeatureCModule,所以也就无法打平FeatureCModule中的providers,那么解决这种问题有两种做法:

第二种做法显然会造成动态加载前后注入不一致的问题,那么只能选择第一种,因为创建了子模块注入器,再加上依赖注入的 解析规则,所以会导致惰性加载的模块注入服务和根模块注入器注入不一致的一些列行为。

Platform 和 Root 注入器

除了惰性加载模块提供的providers外,所有模块的providers@Injectable({providedIn: "root"})供应商都是在 root根注入器中提供的,其实在root之上还有两个注入器,一个是额外的平台ModuleInjector,一个是 NullInjector

image.png

我们简单看一下 Angular 应用的启动过程

platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {...})

对于普通的开发者而言,我们一般接触不到平台注入器和NullInjector,可以假设整个应用程序只有一个根模块注入器(排除懒加载模块的子模块注入器)。

ElementInjector

除了模块注入器外,Angular会为每个 DOM 元素隐式创建ElementInjector

可以用@Component()装饰器中的providersviewProviders属性来配置ElementInjector以提供服务或者依赖值。

@Component({
  ...
  providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent

解析规则

前面已经介绍了 Angular 会有2个层级的注入器,那么当组件/指令解析令牌时,Angular 分为两个阶段来解析它:

Angular 会先从当前组件/指令的ElementInjector查找令牌,找不到会去父组件中查找,直到根组件,如果根还找不到,就去当前组件所在的模块注入器中查找,如果不是懒加载那么就是根注入器,一步一步到最顶层的NullInjector,整个解析过程如下所示:

image.png

解析修饰符

默认情况下,Angular始终从当前的Injector开始,并一直向上搜索,修饰符可以更改开始(默认是自己)或结束位置,从而达到一些高级的使用场景,Angular 中可以使用@Optional()@Self()@SkipSelf()@Host()来修饰 Angular 的解析行为,每个修饰符的说明如下:

@Optional()

@Optional() 允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误。

export class OptionalComponent {
  constructor(@Optional() public optional?: OptionalService) {}
}

@Self()

使用@Self()让 Angular 仅查看当前组件或指令的ElementInjector

@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) {}
}

@Optional()组合使用。

@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 flower?: FlowerService) { }
}

@SkipSelf()

@SkipSelf()@Self()相反,使用@SkipSelf(),Angular 在父ElementInjector中开始搜索服务,而不是从当前 ElementInjector中开始搜索服务。

@Injectable({
    providedIn: 'root'
})
export class FlowerService {
    emoji = '🌿';
    constructor() {}
}
import { Component, OnInit, SkipSelf } from '@angular/core';
import { FlowerService } from '../flower.service';

@Component({
    selector: 'app-skipself',
    templateUrl: './skipself.component.html',
    styleUrls: ['./skipself.component.scss'],
    providers: [{ provide: FlowerService, useValue: { emoji: '🍁' } }]
})
export class SkipselfComponent implements OnInit {
    constructor(@SkipSelf() public flower: FlowerService) {}

    ngOnInit(): void {}
}

上面的示例会得到根注入器中的 🌿,而不是组件所在的ElementInjector中提供的 🍁。

如果值为 null 可以同时使用@SkipSelf()@Optional()来防止错误。

class Person {
  constructor(@Optional() @SkipSelf() parent?: Person) {}
}

@Host()

@Host()使你可以在搜索提供者时将当前组件指定为注入器树的最后一站,即使树的更上级有一个服务实例,Angular 也不会继续寻找。

@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都将使用 🌼(黄色花朵)。

那么问题来了 @Host@Self 到底有什么区别?

@Host 属性装饰器会禁止在宿主组件以上的搜索,宿主组件通常就是请求该依赖的那个组件,不过,当该组件投影进某个父组件时,那个父组件就会变成宿主,意思就是 ng-content 中的组件所在的宿主组件不是自己,而是 ng-content 提供的父组件。比如 <host-comp><sub-comp></sub-comp></host-comp> 中,sub-comp的宿主组件是host-comp

注:

所有修饰符都可以组合使用,但是不能互斥,比如: @Self()@SkipSelf() , @Host()@Self()

在 @Component() 中提供服务

Angular 中所有的依赖都是单例的,由于存在模块和 Element 注入器层级,导致可以在不同的注入器中提供同一个令牌,从而实现非全局单例的效果,在组件中提供服务达到限制某些依赖只能在当前组件/指令以及子元素中使用,或者不同的组件注入各自的单独实例。

在组件/指令中可以通过providersviewProviders分别提供服务依赖项:

@Component({
  ...
  providers: [
    {provide: FlowerService, useValue: {emoji: '🌺'}}
  ]
})
@Component({
  ...
  viewProviders: [
    {provide: AnimalService, useValue: {emoji: '🐶'}}
  ]
})

providers 和 viewProviders

组件或者指令中使用providersviewProviders的区别可以通过阅读 官方文档 hierarchical-dependency-injection#providing-services-in-component 章节深入理解,官方内容有点多,我尝试做一点简化,其实不理解也没有任何问题,实际工作中很少需要使用viewProviders

首先在 Angular 中定义一个如下模板,实际的逻辑结构中会多一个 VIEW 视图的概念:

<app-root>
    <app-child></app-child>
</app-root>
<app-root>
  <#VIEW>
    <app-child>
     <#VIEW>
       ...content goes here...
     </#VIEW>
    </app-child>
  <#VIEW>
</app-root>

如果<app-child></app-child>模板内部有一个 A 组件,A 组件注入服务会先从虚拟的 #VIEW 中查找依赖,然后再到 app-child 组件,解析顺序为app-child #VIEW => app-child => app-root #View => app-root,那么 providers提供的服务其实就是挂载在组件上,viewProviders提供的服务挂载在 #VIEW 这个虚拟结构之上。

正常情况下,providersviewProviders没有任何区别,只有当在组件中使用投影时会不同的表现,比如下面的示例:

<app-root>
    <app-child>
      <a-component></a-component>
    </app-child>
</app-root>

a-component是通过 ng-content 投影传递到 app-child 组件中的,那么如果在 a-component 中注入 FlowerService,此时如果app-child是通过viewProviders提供的依赖,那么 A 组件会找不到依赖值,有投影时实际逻辑图如下:

<app-root>
  <#VIEW>
    <app-child>
     <#VIEW>
       ...content goes here...
     </#VIEW>
     <a-component>
       <#VIEW>
       </#VIEW>
     </a-component>
    </app-child>
  <#VIEW>
</app-root>

此时,a-component 和 app-child 的 #VIEW 是平级的,所以往上找不到 <#VIEW> 中提供的依赖项(也就是 viewProviders提供的依赖性)

解析和查找的逻辑关系如下所示,其实关于viewProviders我们基本上很少使用,所以不理解或者不知道也没有关系,但是通过这个可以深入的理解 Angular 关于视图相关的依赖注入底层逻辑。

<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-inspector>
        <#VIEW>
          <p>Emoji from AnimalService: {{animal.emoji}} (🐳)</p>
        </#VIEW>
      </app-inspector>
     </app-child>
  </#VIEW>
</app-root>

ng-template 注入器

ng-template 在定义的视图层级上下找注入器,并不是在渲染的视图层级找注入器的。这一点特别容易踩坑,当我们编写高度的灵活的组件时经常会支持模板传递,那么渲染模板的节点注入器模板中不一定能够找到,感兴趣不理解的可以看示例: https://stackblitz.com/edit/angular-ivy-9tsdhh

provideIn: “any” | “root” | “platform” | NgModuleType

ElementInjector 使用场景

Angular 依赖注入中的Provider、层级注入器、ElementInjector和模块注入器、注入解析规则等这些知识点是密不可分的,当所有的知识点串起来后再回过头来完整按顺序阅读一下依赖注入的前四个章节,会有一种豁然开朗的感觉,因为我们看到的很多 API 和不同的行为,深层的原因还是 Angular 对于依赖注入的底层设计和原理导致的,读到此处如果前面所有的内容你都有了一个很好的理解,那么恭喜你已经进入到 Angular 依赖注入中高级水平,接下来就是把所学的知识灵活运用从而实现软件架构的升级,提高程序的健壮性和可维护性。

← 返回博客