@ngrx Store按需获取网络数据,是否有更优实现模式?
按需加载Item到NgRx Store的优化方案探讨
我应用中存在大量Item记录,因此采用按需加载至@ngrx Store的方式。若Item已存在于Store内,直接调用select()方法获取,避免重复发起网络请求。请问当前的实现方案是否有更优的替代模式?
我的Facade服务
@Injectable({ providedIn: 'root' }) export class ItemFacade { private readonly store = inject(Store); /** * @description 阻塞操作直至网络请求完成 */ private readonly ready$ = this.store .select(selectLoading) .pipe(filter((loading) => !loading)); /** * @description 从Store中获取Item,若不存在则加载该Item */ getItem(itemId: string): Observable<Item> { // 若Item已存在于Store中,则不会执行该请求 this.store.dispatch(itemAtn.loadItem({ id: itemId })); return this.store.select(selectItem(itemId)).pipe( switchMap((stored) => stored ? of(stored) : this.ready$.pipe( switchMap(() => this.store.select(selectItem(itemId))), filter((item): item is Item => !!item) ) ), first() ); } async getItems(ids: string[]): Promise<Item[]> { return await Promise.all(ids.map((id) => lastValueFrom(this.getItem(id)))); } }
我的Effect
@Injectable() export class ItemEffects { private readonly actions$ = inject(Actions); private readonly store = inject(Store); private readonly itemService = inject(ItemService); /** * @description 若Item未存在于Store中,则从服务器加载该Item,之后将加载标记设为false */ loadItem$ = createEffect(() => this.actions$.pipe( ofType(itemAtn.loadItem), withLatestFrom(this.store.select(selectedAllItems)), // 仅当Item未存在于Store中时才加载 filter(([{ id }, items]) => !items[id]), concatMap(([{ id }]) => this.itemService.getItem$(id).pipe( map((item) => itemAtn.loadItemSuccess({ item })), catchError(() => of(itemAtn.loadItemFailure({ id }))) )), ) ); }
可落地的优化方向
1. 避免重复触发Action与请求防抖
当前每次调用getItem都会无条件dispatch loadItem,尽管Effect会过滤已存在的Item,但短时间内重复调用同一个getItem仍会产生冗余Action。建议在Facade里先检查Store状态,仅当Item不存在且未处于加载状态时才触发dispatch:
// 在Facade中新增加载状态的选择器 private isItemLoading$(itemId: string) { return this.store.select(selectItemLoading(itemId)); } getItem(itemId: string): Observable<Item> { const item$ = this.store.select(selectItem(itemId)); // 仅当Item不存在且未在加载时,才发起请求 forkJoin([item$, this.isItemLoading$(itemId)]).pipe( take(1), filter(([item, isLoading]) => !item && !isLoading) ).subscribe(() => { this.store.dispatch(itemAtn.loadItem({ id: itemId })); }); return item$.pipe( filter(Boolean), first() ); }
2. 替换全局Loading为Item级加载状态
当前用全局loading状态判断请求完成,无法精准对应单个Item的加载状态。建议在State中维护一个loadingIds集合,记录正在加载的Item ID:
// 示例State结构 interface ItemState { entities: Record<string, Item>; loadingIds: Set<string>; } // 对应Effect中更新加载状态 loadItem$ = createEffect(() => this.actions$.pipe( ofType(itemAtn.loadItem), withLatestFrom(this.store.select(selectedAllItems), this.store.select(selectLoadingIds)), filter(([{ id }, items, loadingIds]) => !items[id] && !loadingIds.has(id)), tap(([{ id }]) => this.store.dispatch(itemAtn.loadItemStart({ id }))), // 触发加载开始,将id加入loadingIds concatMap(([{ id }]) => this.itemService.getItem$(id).pipe( map((item) => itemAtn.loadItemSuccess({ item })), catchError(() => of(itemAtn.loadItemFailure({ id }))), finalize(() => this.store.dispatch(itemAtn.loadItemEnd({ id }))) // 加载结束,将id从loadingIds移除 )), ) );
这样在Facade中可以监听单个Item的加载状态,避免全局状态的干扰。
3. 简化Observable流逻辑
当前getItem中的switchMap嵌套可以大幅简化,利用Store的状态推送特性,直接等待Item状态更新即可:
getItem(itemId: string): Observable<Item> { const item$ = this.store.select(selectItem(itemId)); // 检查是否需要发起请求 item$.pipe(take(1)).subscribe(item => { if (!item) { this.store.dispatch(itemAtn.loadItem({ id: itemId })); } }); // 直接等待Item出现,filter确保返回非空值,first取第一个有效结果 return item$.pipe( filter((item): item is Item => !!item), first() ); }
4. 批量请求优化
当前getItems逐个调用getItem,若后端支持批量查询接口,建议改为批量请求减少网络开销:
// Facade中新增批量处理逻辑 getItems(ids: string[]): Observable<Item[]> { // 拆分已存在和需要加载的ID const existingItems$ = this.store.select(selectItemsByIds(ids)); return existingItems$.pipe( take(1), switchMap(existing => { const existingIds = new Set(existing.map(item => item.id)); const needLoadIds = ids.filter(id => !existingIds.has(id)); if (needLoadIds.length === 0) { return of(existing); } // 触发批量加载Action this.store.dispatch(itemAtn.loadItems({ ids: needLoadIds })); // 等待所有需要加载的Item都出现在Store中 return this.store.select(selectItemsByIds(ids)).pipe( filter(items => items.length === ids.length), first() ); }) ); }
同时在Effect中处理批量加载的Action,调用后端批量接口。
5. 完善错误处理
当前Effect仅dispatch失败Action,但Facade未处理错误场景。建议在Store中维护errorIds集合,记录加载失败的Item ID,并在Facade中暴露错误状态:
// Facade中新增错误状态监听 getItemError$(itemId: string): Observable<boolean> { return this.store.select(selectItemError(itemId)); } // 在getItem中加入错误处理 getItem(itemId: string): Observable<Item> { const item$ = this.store.select(selectItem(itemId)); const error$ = this.getItemError$(itemId).pipe( filter(isError => isError), first(), switchMap(() => throwError(() => new Error(`Failed to load item ${itemId}`))) ); // 发起请求逻辑... return merge(item$, error$).pipe( filter((item): item is Item => !!item), first() ); }
内容的提问来源于stack exchange,提问作者Nate May
相关产品推荐
相关产品推荐

