如何强制Angular继承组件调用super.ngOnDestroy或编写通用单元测试
问题1:强制子类重写ngOnDestroy时必须调用super.ngOnDestroy()
有两种落地性很强的方案:
- 模板方法模式(最推荐,从设计层面彻底避免漏调用)
把父类的ngOnDestroy声明为final(TypeScript 4.3及以上支持),禁止子类重写,额外开放一个自定义销毁钩子给子类实现自定义销毁逻辑:
export abstract class SubscriptionManagmentDirective implements OnDestroy { protected componetDestroyed = new Subject<void>() constructor() {} // 用final禁止子类重写,从根源上避免漏调用super的问题 final ngOnDestroy(): void { this.componetDestroyed.next() this.componetDestroyed.unsubscribe() // 调用子类自定义的销毁逻辑 this.onDestroy(); } // 子类要自定义销毁逻辑就重写这个方法,不需要碰ngOnDestroy protected onDestroy(): void { // 子类可选择性重写 } }
这种方案不需要额外的工具校验,子类根本没有重写ngOnDestroy的权限,自然不会出现漏调用super的问题。
- 静态检查 + 运行时校验兜底
如果确实需要允许子类重写ngOnDestroy,可以搭配两层校验:
- 静态检查层:开启ESLint规则
@angular-eslint/use-lifecycle-interface的强化配置,或者添加自定义规则,要求所有重写ngOnDestroy且父类包含ngOnDestroy实现的类,必须调用super.ngOnDestroy()。 - 运行时兜底检查:父类中加入标记位校验,开发环境下如果检测到销毁时没有调用父类方法直接抛错提醒:
export abstract class SubscriptionManagmentDirective implements OnDestroy { private _superNgOnDestroyCalled = false; componetDestroyed = new Subject<void>() constructor() { if (!isProduction) { // 微任务队列检测标记位,开发环境下报错提醒 queueMicrotask(() => { if (this.ngOnDestroy !== SubscriptionManagmentDirective.prototype.ngOnDestroy) { // 子类重写了ngOnDestroy,包装原方法检查super调用 const originalNgOnDestroy = this.ngOnDestroy.bind(this); this.ngOnDestroy = () => { this._superNgOnDestroyCalled = false; originalNgOnDestroy(); if (!this._superNgOnDestroyCalled) { throw new Error(`${this.constructor.name} 重写了ngOnDestroy但未调用super.ngOnDestroy()!`); } } } }) } } ngOnDestroy(): void { this._superNgOnDestroyCalled = true; this.componetDestroyed.next() this.componetDestroyed.unsubscribe() } }
问题2:编写通用单元测试自动覆盖所有继承类
完全可以,根据你用的单元测试框架(Jasmine/Karma或者Jest)可以用文件扫描的方式批量加载所有继承类执行统一测试逻辑,以Jest为例:
// 通用测试用例文件 subscription-management.spec.ts import { SubscriptionManagmentDirective } from './subscription-management.directive'; // 扫描src目录下所有.ts文件,过滤出继承了SubscriptionManagmentDirective的类 const context = require.context('src/app', true, /\.ts$/); const inheritingClasses: Array<new (...args: any[]) => SubscriptionManagmentDirective> = []; context.keys().forEach(filePath => { const fileExports = context(filePath); Object.values(fileExports).forEach(exportVal => { if ( typeof exportVal === 'function' && exportVal.prototype instanceof SubscriptionManagmentDirective ) { inheritingClasses.push(exportVal as any); } }); }); // 批量执行测试用例 describe('所有继承SubscriptionManagmentDirective的组件', () => { it('重写ngOnDestroy时必须调用super.ngOnDestroy', () => { inheritingClasses.forEach(ComponentClass => { // mock依赖注入的参数,创建实例 const instance = new ComponentClass(...Array(ComponentClass.length).fill(null)); const originalNext = spyOn(instance['componetDestroyed'], 'next'); const originalUnsubscribe = spyOn(instance['componetDestroyed'], 'unsubscribe'); instance.ngOnDestroy(); expect(originalNext).toHaveBeenCalled(); expect(originalUnsubscribe).toHaveBeenCalled(); }); }); // 还可以加其他通用校验,比如componetDestroyed是否被正确用于takeUntil等 });
如果用的是Karma+Jasmine,也可以用import.meta.glob或者webpack的require.context实现同样的文件扫描逻辑,不需要给每个子类单独写测试用例,新增继承类时会自动被纳入测试范围。
内容的提问来源于stack exchange,提问作者Lossan
相关产品推荐
相关产品推荐

