Angular组合vs继承:如何保留@Inputs的模板配置能力?
场景背景
日常开发中常遇到这类场景:多个组件共享部分功能,但并非完全一致,直接复制代码会导致重复冗余。比如以下两个组件:
@Component() export class NumberComponent { @Input() numberValue: number = 1; @Input() isDisabled: boolean = false; constructor() {} getNumberValue(): string | null { return this.isDisabled ? null : this.numberValue } } @Component() export class StringComponent { @Input() stringValue: string = 'a'; @Input() isDisabled: boolean = false; constructor() {} getStringValue(): string | null { return this.isDisabled ? null : this.stringValue } }
这些组件存在可抽象的共性,但核心逻辑有本质差异,无法合并为一个带大量条件判断的巨型组件。
现有方案的局限
继承方案
通过抽象基类或无模板指令实现继承是最直接的DRY方案:
@Directive() export abstract class BaseComponent { @Input() isDisabled: boolean = false; } @Component() export class NumberComponent extends BaseComponent { @Input() numberValue: number = 1; constructor() {} getNumberValue(): string | null { return this.isDisabled ? null : this.numberValue } } @Component() export class StringComponent extends BaseComponent { @Input() stringValue: string = 'a'; constructor() {} getStringValue(): string | null { return this.isDisabled ? null : this.stringValue } }
但随着组件输入属性增多,且需要多维度抽象时,继承层级会变得复杂,维护成本急剧上升。
服务组合方案
通过依赖注入的服务实现组合模式,扩展性更强,可按功能拆分不同服务按需注入:
@Injectable() export class BaseService { isDisabled: boolean = false; } @Component() export class NumberComponent { @Input() numberValue: number = 1; constructor(private baseService: BaseService) {} getNumberValue(): string | null { return this.baseService.isDisabled ? null : this.numberValue } } @Component() export class StringComponent { @Input() stringValue: string = 'a'; constructor(private baseService: BaseService) {} getStringValue(): string | null { return this.baseService.isDisabled ? null : this.stringValue } }
但该方案的核心问题是:服务中的属性无法直接通过模板的@Input()配置。原本继承模式下的模板配置方式失效:
<app-number [numberValue]="2" [isDisabled]="true"> </app-number>
只能在父组件中通过服务手动设置,导致配置分散在模板和组件代码中,降低开发体验。
可行解决方案探索
1. 用指令封装共享输入属性
创建包含共享输入属性的指令,在目标组件中注入该指令,将指令的属性暴露给组件模板或逻辑使用:
@Directive({selector: '[sharedDisabled]'}) export class SharedDisabledDirective { @Input() isDisabled: boolean = false; } @Component({ selector: 'app-number', template: `...`, hostDirectives: [SharedDisabledDirective] // Angular 14+ 支持的宿主指令 }) export class NumberComponent { @Input() numberValue: number = 1; constructor(private disabledDirective: SharedDisabledDirective) {} getNumberValue(): string | null { return this.disabledDirective.isDisabled ? null : this.numberValue } }
模板中可直接通过[isDisabled]配置:
<app-number [numberValue]="2" [isDisabled]="true"> </app-number>
旧版本Angular可通过@ViewChild或@ContentChild获取指令实例,同样能实现属性同步。
2. 为服务属性添加@Input()代理
使用组件级注入的服务(每个组件实例拥有独立服务实例),在组件中创建@Input()属性,通过setter同步服务内的状态:
@Injectable({providedIn: 'component'}) // 组件级注入,每个组件实例对应一个服务实例 export class BaseService { isDisabled: boolean = false; } @Component() export class NumberComponent { @Input() numberValue: number = 1; // 用@Input()代理服务属性 @Input() set isDisabled(val: boolean) { this.baseService.isDisabled = val; } get isDisabled(): boolean { return this.baseService.isDisabled; } constructor(private baseService: BaseService) {} getNumberValue(): string | null { return this.baseService.isDisabled ? null : this.numberValue } }
这种方式既保留了组合模式的扩展性,又能通过模板[isDisabled]直接配置,状态同步完全透明。
3. 使用组件级注入的可注入实体
利用providedIn: 'component'的可注入类,结合组件的@Input()实现状态绑定。本质和方案2类似,但可将共享逻辑拆分为更细粒度的可注入单元:
@Injectable({providedIn: 'component'}) export class DisabledStateService { private _isDisabled = false; get isDisabled(): boolean { return this._isDisabled; } set isDisabled(val: boolean) { this._isDisabled = val; // 可在此添加状态变化的逻辑,比如通知订阅者 } } @Component() export class StringComponent { @Input() stringValue: string = 'a'; @Input() set isDisabled(val: boolean) { this.disabledStateService.isDisabled = val; } get isDisabled(): boolean { return this.disabledStateService.isDisabled; } constructor(private disabledStateService: DisabledStateService) {} getStringValue(): string | null { return this.disabledStateService.isDisabled ? null : this.stringValue } }
该方案适合将共享逻辑(如状态变更监听、业务规则)封装在可注入类中,组件仅负责属性透传和自身核心逻辑。
内容的提问来源于stack exchange,提问作者nate-kumar

