0%

angular-di-resolution-modifier

In Angular, the resolution modifier is used to control how the dependency injector resolves dependencies. There are four resolution modifiers in Angular:

先来给这几个操作符分个类:

  1. @SkipSelf - 何时开始查找。
  2. @Self, @Host - 何时停止查找。
  3. @Optional - 找不到时怎么办。

@Optional

这个是最简单了修饰符了,它告诉Angular依赖注入器,如果找不到依赖项,不要抛出错误,而是返回null

大家都见过下面这个错误吧,当Angular找不到某个依赖时,就会抛出这个错误:(注意:这种错误一般发生在某个Service没有提供{providedIn: 'root'},而且该Service的使用者也没有在providers数组中提供该Service时。)

1
2
3
4
5
6
7
8
9
10
11
12
ERROR NullInjectorError: R3InjectorError(Environment Injector)[_OrderService -> _OrderService]: 
NullInjectorError: No provider for _OrderService!
at NullInjector.get (core.mjs:1635:21)
at R3Injector.get (core.mjs:3017:27)
at R3Injector.get (core.mjs:3017:27)
at ChainedInjector.get (core.mjs:5288:32)
at lookupTokenUsingModuleInjector (core.mjs:5631:31)
at getOrCreateInjectable (core.mjs:5677:10)
at Module.ɵɵdirectiveInject (core.mjs:11586:17)
at NodeInjectorFactory.ProductComponent_Factory [as factory] (product.component.ts:21:30)
at getNodeInjectable (core.mjs:5871:38)
at createRootComponent (core.mjs:16405:31)

如果我们在依赖注入的时候加上@Optional()修饰符,那么Angular就不会抛出错误,而是返回null

1
2
3
constructor(@Optional() private orderService: OrderService) {
this.orderService.fetchData(1).then(console.log);
}

注意:上面这种写法,在使用orderService时,需要判断是否为null,否则仍然会抛出Cannot read properties of null (reading 'fetchData')的错误。

我们可以针对orderService使用?(Optional Chaining Operator),就不会抛出错误了:

1
2
3
constructor(@Optional() private orderService: OrderService) {
this.orderService?.fetchData(1).then(console.log);
}

@Self

@Self也比较简单,就是告诉Angular只在当前Component或者Directive中搜索某个依赖项,而不会去父级Component或者Directive中搜索。假设有OrderService,且是providedIn: 'root'的。

1
2
@Injectable({providedIn: "root"})
export class OrderService {}

如果我们在注入该Service时,加上@Self()修饰符,那么Angular只会在当前Component或者Directive中搜索该Service,但是当前Component或者Directive中没有提供该Service时,所以就会抛出错误。

1
2
3
constructor(@Self() private orderService: OrderService) {
this.orderService.fetchData(1).then(console.log);
}

注意观察这个错误和上面错误的区别,这个错误的顶端不再是NullInjector了,因为我们只在当前Component中查找,所以永远不会到达NullInjector这一步。而NodeInjector是当前Component的Injector。

1
2
3
4
5
6
7
8
9
10
11
core.mjs:7191 ERROR RuntimeError: NG0201: No provider for _OrderService found in NodeInjector. Find more at https://angular.dev/errors/NG0201
at throwProviderNotFoundError (core.mjs:986:9)
at notFoundValueOrThrow (core.mjs:5606:5)
at lookupTokenUsingModuleInjector (core.mjs:5639:10)
at getOrCreateInjectable (core.mjs:5677:10)
at Module.ɵɵdirectiveInject (core.mjs:11586:17)
at NodeInjectorFactory.ProductComponent_Factory [as factory] (product.component.ts:21:30)
at getNodeInjectable (core.mjs:5871:38)
at createRootComponent (core.mjs:16405:31)
at ComponentFactory.create (core.mjs:16262:21)
at ViewContainerRef2.createComponent (core.mjs:16662:43)

@Self@Optional是可以配合使用的,为了修复上面的错误,我们可以这样写,这样就不会报错了。

1
2
3
constructor(@Self() @Optional() private orderService: OrderService) {
this.orderService?.fetchData(1).then(console.log);
}

所以,@Self修饰符的使用场景到底是啥?请继续往下看。

Multiple Directives in same Dom elements

如果一个DOM元素上应用了多个Directive, 那么当第一个Directive提供了某个Service时,后面的Directive就可以通过@Self来获取这个Service。

下面的代码,假设dir1对应Directive1,dir2对应Directive2,如果Directive1中的providers数组提供了某个Service,那么Directive2中constructor可以使用@Self来获取Service。

1
<p dir1 dir2>Hello, world!</p>

Directive1中提供了OrderService

1
2
3
4
5
6
7
8
// Directive1
@Directive({
selector: '[dir1]',
providers: [OrderService],
})
export class Dir1Directive {
constructor() { }
}

Directive2中使用@Self来获取OrderService。

1
2
3
4
5
6
7
// Directive2
@Directive({
selector: '[dir2]'
})
export class Dir2Directive {
constructor(@Self() private orderService: OrderService) { }
}

为什么这样可以呢?因为dir1dir2位于同一个dom元素-p标签上,Angular会为这个p标签创建一个Injector,dir1dir2会share这个Injector,所以dir2可以通过@Self来获取dir1中提供的OrderService。

@Host

@Host修饰符告诉Angular依赖注入器,只在当前Component或者Host元素中搜索某个依赖项。

  • 当不使用ng-content时,Host就是注入依赖的Component。
  • 当使用ng-content时,Host就是投射内容的Component(也就是ng-content所在的Component)。see here for details.

@Host的作用体现在使用ng-content进行内容投射的时候。下面的例子中,我们有一个ProductComponent,它的模板中有ng-content,我们将ProductDetailComponent投射到ProductComponent中。- 此时,ProductComponent就是ProductDetailComponent的Host。

1
2
3
4
5
6
<!-- product.component.html -->
<div class="container">
<div class="header"></div>
<ng-content></ng-content>
<div class="footer"></div>
</div>

然后我们在ProductComponent中提供OrderService,并在ProductDetailComponent中使用@Host来获取OrderService。

1
2
// product.component.ts
providers: [OrderService],
1
2
// product-detail.component.ts
constructor(@Host() private orderService: OrderService) {}

最后在AppComponent的模板中使用ProductComponent

1
2
3
4
<!--app.component.html-->
<app-product>
<app-product-detail></app-product-detail>
</app-product>

这时,app-product就是app-product-detailHost,如果我们在ProductDetailComponent中使用@Host来获取OrderService,那么Angular会首先在ProductDetailComponent中查找,结果没找到,然后再去ProductComponent中查找OrderService,结果找到了。

@SkipSelf

@SkipSelf就是查找依赖时不在当前Component或者Directive中查找,而是跳过当前Component或者Directive,去父级Component或者Directive中查找。

所以,@SkipSelf修饰符的使用场景到底是啥?

References:

  1. https://v17.angular.io/guide/dependency-injection-in-action#make-a-dependency-optional-and-limit-search-with-host - 关于Host的定义,可以看这里。