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

NGRX中:合并Banner与图片URL数据,选Effect调用还是Selector处理?

问题描述

我有一个带属性的Banner列表,模型定义如下:

export interface Banner {
    id: number;
    name: string;
    channelId: string;
    language: string;
    zoneId: string;
    priority: number;
    fileId: string;
    startDate: Date;
    endDate: Date;
    active: boolean;
    labels: string[];
}

我的服务会返回上述所有属性,但需要通过第一个服务返回的fileId调用第二个服务获取图片URL。目前我正在使用NGRX状态管理,已经定义了相关Actions和Effects:

Actions定义

export const getImageById = createAction(
    BannerActionTypes.GetImageById,
    props<{ id: string }>()
);
export const getImageByIdSuccess = createAction(
    BannerActionTypes.GetImageByIdSuccess,
    props<{ url: string }>()
);
export const getImageByIdFail = createAction(
    BannerActionTypes.GetImageByIdFail,
    props<{ error: Error | any }>()
);

export const loadBanners = createAction(
    BannerActionTypes.LoadBanners,
    props<{ filter: Filter }>()
);
export const loadBannersSuccess = createAction(
    BannerActionTypes.LoadBannersSuccess,
    props<{ response: BannerResponse }>()
);
export const loadBannersFail = createAction(
    BannerActionTypes.LoadBannersFail,
    props<{ error: Error | any }>()
);

Effects定义

export class BannerEffect {
    constructor(private actions$: Actions, private bannersService: BannersService) { }

    loadBanners$ = createEffect(() =>
        this.actions$.pipe(
            ofType(BannerActions.loadBanners),
            switchMap((s) =>
                this.bannersService.getAll(s.filter).pipe(
                    map((response) => BannerActions.loadBannersSuccess({ response })),
                    catchError((error) => of(BannerActions.loadBannersFail({ error })))
                )
            )
        )
    );
    getImage$ = createEffect(() =>
        this.actions$.pipe(
            ofType(BannerActions.getImageById),
            switchMap((s) =>
                this.bannersService.getImageById(s.id).pipe(
                    map((url) => BannerActions.getImageByIdSuccess({ url })),
                    catchError((error) => of(BannerActions.getImageByIdFail({ error })))
                )
            )
        )
    );
}

现在想咨询:应该在loadBanners Effect加载成功后调用getImageById,还是在Selector中合并这两个服务调用?哪种是最佳方案?我希望将所有属性与图片URL整合在一处。


最佳方案分析

方案1:在loadBanners Effect成功后批量调用图片接口(推荐)

这是完全符合NGRX架构原则的方案,核心优势如下:

  • 状态一致性:所有异步数据获取逻辑都收敛在Effects中,状态更新流程清晰可追溯,组件只需订阅状态,无需处理异步逻辑。
  • 批量处理与缓存:可以在loadBannersSuccess触发后,批量发起图片请求;还能在状态中维护fileId到url的映射表,过滤已请求过的fileId,避免重复请求。
  • 错误隔离:单个图片请求失败不会影响整个Banner列表的展示,还能给每个Banner单独标记图片加载状态(加载中/成功/失败),方便组件做UI适配。

具体实现调整:

  1. 更新State结构:新增存储图片URL映射和加载状态的字段
export interface BannerState {
    banners: Banner[];
    fileUrlMap: Record<string, string>; // 存储fileId对应图片URL
    imageLoadingStatus: Record<string, 'loading' | 'success' | 'failed'>; // 跟踪每个图片的加载状态
    // 原有其他状态字段...
}
  1. 新增Effect处理批量图片加载
loadBannerImages$ = createEffect(() =>
    this.actions$.pipe(
        ofType(BannerActions.loadBannersSuccess),
        switchMap(({ response }) => {
            // 过滤已缓存的fileId,避免重复请求
            const unloadedFileIds = response.banners
                .map(b => b.fileId)
                .filter(id => !this.store.selectSnapshot(state => state.banners.fileUrlMap[id]));
            
            if (unloadedFileIds.length === 0) {
                return EMPTY;
            }

            // 批量发起图片请求,forkJoin同时处理多个请求
            return forkJoin(
                unloadedFileIds.map(fileId => 
                    this.bannersService.getImageById(fileId).pipe(
                        map(url => ({ fileId, url })),
                        catchError(() => of({ fileId, url: null })) // 单独处理单个图片请求失败
                    )
                )
            ).pipe(
                map(results => BannerActions.batchImageLoadSuccess({ results })),
                catchError(() => of(BannerActions.batchImageLoadFail()))
            );
        })
    )
);
  1. 新增对应Actions
export const batchImageLoadSuccess = createAction(
    BannerActionTypes.BatchImageLoadSuccess,
    props<{ results: Array<{ fileId: string; url: string | null }> }>()
);
export const batchImageLoadFail = createAction(
    BannerActionTypes.BatchImageLoadFail
);
  1. 更新Reducer处理状态更新
case BannerActionTypes.BatchImageLoadSuccess:
    const newFileUrlMap = { ...state.fileUrlMap };
    const newLoadingStatus = { ...state.imageLoadingStatus };
    action.results.forEach(result => {
        newFileUrlMap[result.fileId] = result.url;
        newLoadingStatus[result.fileId] = result.url ? 'success' : 'failed';
    });
    return {
        ...state,
        fileUrlMap: newFileUrlMap,
        imageLoadingStatus: newLoadingStatus
    };

方案2:在Selector中合并数据(不推荐)

如果在Selector中调用服务获取图片URL,会直接违背NGRX的核心原则:

  • Selector必须是纯函数,只能从现有状态派生数据,不能包含异步操作或副作用。
  • 这种做法会导致组件订阅Selector时重复发起请求,无法控制加载状态和错误处理,也难以实现缓存逻辑。

最终结论

优先选择方案1,在Effects中完成Banner列表和图片URL的异步获取与状态整合,最后通过Selector将Banner数据和对应图片URL组合成完整结构供组件使用:

整合数据的Selector示例

export const selectBannersWithImages = createSelector(
    selectBannerState,
    state => state.banners.map(banner => ({
        ...banner,
        imageUrl: state.fileUrlMap[banner.fileId],
        imageStatus: state.imageLoadingStatus[banner.fileId] || 'loading'
    }))
);

组件只需订阅selectBannersWithImages就能拿到包含图片URL的完整Banner数据,所有异步逻辑都封装在状态层,组件保持简洁易维护。

内容的提问来源于stack exchange,提问作者elene

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 12:35:43