You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 04:55:03