是否应避免嵌套RxJS操作符?Angular中含嵌套操作符的Effect测试失败问题及优化咨询
让我们逐个解决你的疑问:
1. 是否应该避免嵌套RxJS操作符?
嵌套RxJS操作符本身不是必须禁止的行为——关键在于可读性和可维护性。如果嵌套层级不深(比如2-3层),且逻辑清晰,完全可以保留。但如果嵌套超过3层,或者逻辑变得晦涩,就需要考虑重构为更扁平的写法。
你的当前代码嵌套层级只有两层(switchMap内部嵌套from+mergeMap+toArray),逻辑是清晰的,但确实可以优化成更简洁的扁平写法,同时保持行为一致。
2. 更优的实现方式
你的需求是并行请求所有id对应的资源,然后收集所有结果分发动作。这里用forkJoin替代from+mergeMap+toArray是更合适的选择,因为forkJoin专门用于处理“等待多个Observable完成,然后返回结果数组”的场景,代码更简洁且语义明确:
public loadResources$: Observable<MyAction> = this.actions$.pipe( ofType(MyActions.myAction), // 直接将ids数组映射为Observable数组,用forkJoin并行执行 switchMap(({ ids }) => forkJoin(ids.map(id => this.myApiService.getResource(id)))), map(resources => MyActions.resourcesLoaded({ resources })) );
为什么这个写法更好?
- 没有嵌套操作符,代码更扁平,可读性更强
forkJoin的语义直接对应“并行请求+收集所有结果”的需求,无需额外的from和toArray- 行为和你的原代码完全一致:所有HTTP请求并行发起,等全部完成后才分发
resourcesLoaded动作
如果你的需求是顺序请求(一个请求完成后再发起下一个),可以用concatMap配合toArray,但显然你的场景并行请求更高效。
3. 测试用例无法通过的问题修复
你的测试失败主要有3个原因,我们逐个修正:
问题1:返回值类型不匹配
你在values里用t对应ids[0],但myApiService.getResource(id)返回的是Observable<Resource>,所以模拟返回的Observable应该发出Resource对象,而不是id。
问题2:Action名称笔误
你的Effect监听的是MyActions.myAction,但测试里触发的是MyActions.loadResources({ ids }),这会导致Effect根本不会触发。
问题3:时间线计算错误
Jasmine Marbles的时间线中,每个-代表10帧。你需要正确计算动作发出、请求完成、结果返回的时间节点。
修正后的测试代码:
it('should dispatch an resourcesLoaded action with the resources', () => { const ids = ['5f7c723832758b859bd8f866']; const mockResource = {} as Resource; const mockResources = [mockResource]; const values = { // 修正:触发Effect的action是MyActions.myAction trigger: MyActions.myAction({ ids }), // 修正:getResource返回的是Resource对象 resource: mockResource, // 预期分发的动作 loaded: MyActions.resourcesLoaded({ resources: mockResources }) }; // 动作在第20帧发出(--trigger) actions = hot('--trigger----------------', values); // getResource订阅后,50帧返回结果(-----resource) const mockGetResource$ = cold('-----resource', values); // 计算预期时间:20(动作发出) + 50(请求完成) = 70帧,对应-------loaded const expected = cold('-------loaded------------', values); myApiService.getResource.withArgs(ids[0]).returns(mockGetResource$); expect(myEffects.loadResources$).toBeObservable(expected); });
额外提示:用forkJoin后的测试兼容性
如果你改用了forkJoin的优化写法,上述测试代码依然可以正常工作,因为forkJoin的行为是等待所有请求完成后返回数组,和原代码的mergeMap+toArray最终结果一致。
内容的提问来源于stack exchange,提问作者Jacopo Lanzoni

