Angular中实现多态@ViewChildren选择器的简便方法?
问题背景
我在开发Angular应用时,需要用@ViewChildren查询一组具备共同特性(比如通用方法)的组件,这些组件并非同一类,但都实现了IGadget接口、继承了AbstractGadget抽象类。目前每个组件都必须手动在@Component的providers里配置{ provide: AbstractGadget, useExisting: 当前组件类 },才能让@ViewChildren通过抽象类查询到它们,代码重复度很高,想找更简洁的实现方式。当前用的是Angular 12,升级版本没问题。
优化方案
1. 自定义装饰器自动注入providers
写一个自定义装饰器,替我们自动添加重复的providers配置,不用每个组件都写一遍:
import { Component, ComponentDecorator } from '@angular/core'; import { AbstractGadget } from './abstract-gadget'; /** * 替代原生@Component,自动添加AbstractGadget的DI配置 */ function GadgetComponent(config: Parameters<ComponentDecorator>[0]): ClassDecorator { return (target: Function) => { // 合并原有配置与自动添加的providers const mergedConfig = { ...config, providers: [ ...(config.providers || []), { provide: AbstractGadget, useExisting: target } ] }; // 调用原生Component装饰器 Component(mergedConfig)(target); }; }
然后在组件里用这个装饰器代替原生的@Component:
@GadgetComponent({ selector: 'first-gadget', templateUrl: './first-gadget.component.html' }) class FirstGadget extends AbstractGadget implements IGadget { // 组件逻辑 }
这样所有Gadget组件都不用再手动写providers配置了,装饰器会自动处理。
2. 用统一指令作为查询标识
如果不想依赖抽象类的DI配置,可以创建一个空指令,让所有Gadget组件都带上这个指令,以此作为@ViewChildren的查询标识:
首先创建指令:
import { Directive, Host } from '@angular/core'; import { IGadget } from './gadget.interface'; @Directive({ selector: '[gadget]' }) export class GadgetDirective { // 通过@Host注入宿主组件的IGadget实例 constructor(@Host() public instance: IGadget) {} }
然后在模板中给每个Gadget组件添加指令属性:
<first-gadget gadget></first-gadget> <second-gadget gadget></second-gadget>
最后在父组件中查询这个指令:
import { ViewChildren, QueryList } from '@angular/core'; import { GadgetDirective } from './gadget.directive'; // ... @ViewChildren(GadgetDirective) gadgets!: QueryList<GadgetDirective>; // 调用通用方法示例 ngAfterViewInit() { this.gadgets.forEach(gadget => gadget.instance.commonMethod()); }
这个方案不需要每个组件配置providers,通过指令统一标识,更简洁。
3. 升级Angular版本的补充说明
升级到Angular 14+能获得更好的类型推断支持,Angular 16+的信号(Signals)也能让视图查询的写法更简洁,但这些升级主要是优化语法和类型体验,核心的冗余问题还是需要上面两种方案解决——毕竟@ViewChildren本质是基于DI标识或指令/组件类型查询,语言层面的接口/继承关系无法被它直接识别。
总结
- 想保留基于抽象类的查询逻辑:用自定义装饰器消除重复的providers配置,兼容Angular 12+;
- 想彻底简化查询逻辑:用统一指令作为查询标识,代码更清爽;
- 升级Angular版本能提升开发体验,但无法替代上述两种方案解决冗余问题。
内容的提问来源于stack exchange,提问作者mhd

