Angular10教程–3.2 依赖注入-多级注入器

Angular 中的注入器有一些规则,你可以利用这些规则来在应用程序中获得所需的可注入对象可见性。通俗的讲,这些规则指定了注入器的“作用域"。
Angular10教程--3.2 依赖注入-多级注入器

两个注入器层次结构

Angular 中有两个注入器层次结构:
  1.  层次结构 —— 在 @NgModule() 或 @Injectable() 中提供的服务。
  2.  层次结构 —— 在 @Directive() 或 @Component() 中提供的服务。

服务查找规则(解析规则)

  1. 优先查找ElementInjector,如果当前组件找不到提供者,将会去父组件中的ElementInjector  
  2. 当所有的ElementInjector都找不到,就会去中找
  3. 如果ModuleInjector也找不到,就会抛出一个错误
  4. 对于同名的令牌,只会解析遇到的第一个依赖

解析修饰符

默认情况下,Angular 始终从当前的 Injector 开始,并一直向上搜索。但修饰符使你可以更改开始(默认是自己)或结束位置。

  • 如果 Angular 找不到你要的服务怎么办,用 @Optional()阻止报错
  • 用 @SkipSelf()跳过自身,从父组件(指令)开始找
  • @Self()只在当前组件(指令)找
  • @Host()只在当前组件(指令)宿主上找
我们将创建一个logger组件和一个logger服务来验证上面的规则:
// logger.service.ts
import { Injectable } from '@/core';
@Injectable()
export class LoggerService {
  constructor() { }
  log(message: string) {
    console.log(message);
  }
}
// logger.component.ts
...
@Component({
  selector: 'app-logger',
  template: `<p>logger works!</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoggerComponent implements OnInit {
  constructor(private loggerservice: LoggerService) { }
  ngOnInit(): void {
    this.loggerservice.log('这是组件内打印的日志');
  }
}
这样,我们没有在任何地方提供logger服务,浏览器会报没有提供服务的错:

Angular10教程--3.2 依赖注入-多级注入器

当我们给服务加上@Optional()装饰器:

// logger.component.ts
constructor(@Optional() private loggerservice: LoggerService) { }
在看浏览器日志,原来没有提供服务的错误已经修复:

Angular10教程--3.2 依赖注入-多级注入器

当我们将在组件内providers没有提供服务,而在根模块中提供logger服务:
// app.module.ts
@NgModule({
  //...
  providers: [LoggerService],
})
我们明确知道自身组件没有提供服务时,可以添加@SkipSelf()跳过自身:
// logger.component.ts
constructor(@Optional() @SkipSelf() private loggerservice: LoggerService) { }
服务依旧可用:Angular10教程--3.2 依赖注入-多级注入器

@Self()同理,当明确知道只需要在自身组件内找服务时使用。

对于@Self()@Host()的区别,官网有这么一句话:

@Host()会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。

那我们就使用投影来解释下这个区别。

ng g c components/logger/logger-content -s -t
// logger.component.ts
@Component({
  selector: 'app-logger',
  template: `
  <div>
    <p>logger works!</p>
    <ng-content></ng-content>
  </div>
  `,
  providers: [LoggerService]
})

调用logger-content组件:

<app-logger>
  <app-logger-content></app-logger-content>
</app-logger>

上面以内容投影的方式将logger-content组件引入到logger组件,并只在logger组件提供服务。

// logger-content.component.ts
export class LoggerContentComponent implements OnInit {
  // 使用@Host()装饰器
  constructor(@Host() private loggerservice: LoggerService) { }
  ngOnInit(): void {
    this.loggerservice.log('logger-content 组件打印的内容!');
  }
}
通过@Host(),我们是可以获取到logger-service的,即用@Host()依然能访问父组件的ElementInjector
我们知道,如果logger-content组件不是通过投影的方式引入,app-logger-content就是它的宿主。但是通过投影方式引入,引入它的组件就是它的宿主。(不太确定这样的说法是否正确,但通过@Host()装饰器的解释,或许可以这样理解)

其实一般情况下,@Host()可以完全替换@Self()的,他们之间的区别真的很微妙。

指定NgModule的注入

之前providedIn: 'root'指定从根模块中提供该服务,其实也可以像下面这样指定其它模块提供该服务。(这里有个前提:我们是将logger组件分配到ComponentsModule模块的)
// logger.service.ts
@Injectable({
  providedIn: ComponentsModule
})
如果这么做,意味着这个服务无法在模块该模块内部使用,编辑器会报一个循环依赖的警告:Angular10教程--3.2 依赖注入-多级注入器

浏览器也会报错:

Angular10教程--3.2 依赖注入-多级注入器

所以,只能在其它引入了ComponentsModuleNgModule中,才能使用该服务。

viewProviders

使用 viewProviders 数组是在 @Component() 装饰器中提供服务的另一种方法。
上面说了,组件会从自身开始寻找服务,然后遍历父组件的ElementInjector,直到找到为。viewProviders也有这个特性,区别是,对投影内容不可见。
viewProviders的修饰符与providers的修饰符用法一致。
<app-logger>
  <app-logger-content></app-logger-content>
</app-logger>
LoggerContentComponent无法找到LoggerComponent里用viewProviders提供的服务。

总结

  1. Angular提供两个注入器层次结构:ModuleInjector层次结构及ElementInjector 层次结构;
  2. 服务查找规则可以通俗的理解为‘就近原则’;
  3. 通过配置不同的解析修饰符,可以对程序进行一定程度的优化;
  4. viewProviders对投影内容不可见。

本文转载自:岩弈,版权归原作者所有,本博客仅以学习目的的传播渠道,不作版权和内容观点阐述,转载时根据场景需要有所改动。