迁移NgRx Effects后createEffect回调提前执行致Angular DI依赖未定义
NgRx Effects迁移后单元测试依赖注入异常问题排查与解决
问题描述
迁移NgRx Effects类到新仓库后,单元测试出现依赖注入(DI)异常:类中fetchSingleStuff$属性通过createEffect定义的回调函数,在构造函数执行前就已运行,导致注入的actions$和MyService服务未定义。旧仓库中该回调会在构造函数执行后运行,依赖均正常。
相关代码与输出
简化Effects类代码
import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { MyService } from '../services/my.service'; import { stuffActions } from './stuff.actions'; import { map, switchMap } from 'rxjs/operators'; @Injectable() export class StuffEffects { fetchSingleStuff$ = createEffect(() => { console.log('回调执行,actions$:', this.actions$, 'MyService:', this.myService); return this.actions$.pipe( ofType(stuffActions.fetchSingleStuff), switchMap(({ id }) => this.myService.getStuff(id)), map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff })) ); }); constructor( private actions$: Actions, private myService: MyService ) { console.log('构造函数执行'); } }
单元测试代码
import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { Observable } from 'rxjs'; import { StuffEffects } from './stuff.effects'; import { MyService } from '../services/my.service'; import { MockProvider } from 'ng-mocks'; describe('StuffEffects', () => { let effects: StuffEffects; let actions$: Observable<any>; beforeEach(() => { TestBed.configureTestingModule({ providers: [ StuffEffects, provideMockActions(() => actions$), MockProvider(MyService) ] }); effects = TestBed.inject(StuffEffects); }); it('should be created', () => { expect(effects).toBeTruthy(); }); });
新旧仓库控制台输出对比
旧仓库输出:
构造函数执行
回调执行,actions$: [ActionsObservable], MyService: [MyService]新仓库输出:
回调执行,actions$: undefined, MyService: undefined
构造函数执行
新旧tsconfig配置差异
旧仓库(关键部分)
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": false, // 其他配置... } }
新仓库(关键部分)
{ "compilerOptions": { "target": "ES2022", "useDefineForClassFields": true, // 其他配置... } }
问题原因
核心差异在于TypeScript的useDefineForClassFields配置:
- 当该配置设为
true时,类属性会遵循ECMAScript标准的类字段定义逻辑,在构造函数执行前完成求值。此时createEffect的回调会提前运行,而this还未完成依赖注入,导致依赖对象未定义。 - 旧仓库该配置为
false,类属性初始化逻辑嵌入构造函数执行流程,因此回调会在依赖注入完成后才运行。
解决办法
方案1:对齐tsconfig配置
将新仓库的useDefineForClassFields设为false,与旧仓库保持一致:
{ "compilerOptions": { "useDefineForClassFields": false, // 其他配置... } }
方案2:改用函数式Effect写法(推荐)
使用NgRx 14+推荐的函数式模式,通过inject直接获取依赖,避免依赖类的this:
@Injectable() export class StuffEffects { fetchSingleStuff$ = createEffect( (actions$ = inject(Actions), myService = inject(MyService)) => { console.log('回调执行,actions$:', actions$, 'MyService:', myService); return actions$.pipe( ofType(stuffActions.fetchSingleStuff), switchMap(({ id }) => myService.getStuff(id)), map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff })) ); }, { functional: true } ); constructor() { console.log('构造函数执行'); } }
方案3:延迟属性初始化
将Effect属性改为getter,延迟到构造函数执行后才触发初始化:
@Injectable() export class StuffEffects { get fetchSingleStuff$() { return createEffect(() => { console.log('回调执行,actions$:', this.actions$, 'MyService:', this.myService); return this.actions$.pipe( ofType(stuffActions.fetchSingleStuff), switchMap(({ id }) => this.myService.getStuff(id)), map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff })) ); }); } constructor( private actions$: Actions, private myService: MyService ) { console.log('构造函数执行'); } }
内容的提问来源于stack exchange,提问作者TabsNotSpaces
相关产品推荐
相关产品推荐

