Angular中使用ngrx/store时硬导航如何按需重载数据
NgRx按需加载数据及硬导航场景落地方案
首先明确两个核心问题:
- 你当前的实现把全量数据加载逻辑耦合在列表组件构造函数中,用户直接访问详情页时列表组件未实例化,Store自然没有缓存数据,这是逻辑耦合导致的必然问题。
- Angular框架中,构造函数仅适合做依赖注入,不要在构造函数中编写数据拉取这类业务副作用,组件级的一次性初始化逻辑应该使用
ngOnInit生命周期钩子实现,这是框架提供的标准一次性初始化入口。
针对你已经引入@ngrx/router-store的技术栈,有三种成熟方案实现数据按需重载,完全覆盖硬导航场景:
方案1:详情页自校验加载(改造成本最低)
不依赖列表页的加载逻辑,在详情页初始化时主动校验Store中是否存在当前路由参数对应的条目,不存在则主动触发加载动作。
// Item-Details-Component 实现 import { take, switchMap, tap } from 'rxjs'; import { getSelectors } from '@ngrx/router-store'; import { getItemById } from './store/items.selectors'; import { loadItemById, loadItems } from './store/items.actions'; export class ItemDetailsComponent implements OnInit { // 直接选当前需要的详情数据,模板层用async管道订阅即可 item$ = this.store.pipe(select(getItemById(this.currentItemId))); private currentItemId: string | number = ''; constructor(private store: Store<fromReducer.State>) {} ngOnInit(): void { const { selectRouteParams } = getSelectors(); this.store.pipe( select(selectRouteParams), take(1), tap(params => this.currentItemId = params['id']), switchMap(params => this.store.pipe( select(getItemById(params['id'])), take(1), tap(existItem => { if (!existItem) { // 接口支持单条查询就拉单条,不支持就触发全量加载 this.store.dispatch(loadItemById({ id: params['id'] })); // 无单条接口替换为 this.store.dispatch(loadItems()) } }) )) ).subscribe(); } }
这种方案改造成本极低,不管用户是从列表页跳转还是直接输入URL硬导航,只要进入详情页就会自动校验数据,缺量自动补拉。
方案2:全局Effect监听路由加载(耦合度最低)
@ngrx/router-store会把所有路由跳转动作派发到Store,你可以把数据加载逻辑完全抽离到NgRx Effect中,和组件生命周期彻底解耦,是NgRx技术栈下的推荐实践。
// items.effects.ts import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; import { ofType, createEffect } from '@ngrx/effects'; import { map, filter, switchMap, catchError, take } from 'rxjs'; import { of } from 'rxjs'; import { selectAllItemsLoaded, getItemById } from './store/items.selectors'; import { loadItemsSuccess, loadItemsFailure, loadItemByIdSuccess, loadItemByIdFailure } from './store/items.actions'; @Injectable() export class ItemsEffects { // 监听进入详情页路由,自动校验加载单条数据 loadDetailOnRouteEnter$ = createEffect(() => this.actions$.pipe( ofType(ROUTER_NAVIGATION), filter((action: RouterNavigationAction) => { const url = action.payload.routerState.url; return url.startsWith('/items/') && !!action.payload.routerState.params['id']; }), switchMap(action => { const itemId = action.payload.routerState.params['id']; return this.store.pipe( select(getItemById(itemId)), take(1), // 已有缓存数据直接跳过,不发重复请求 filter(existItem => !existItem), switchMap(() => this.itemsService.getById(itemId).pipe( map(item => loadItemByIdSuccess({ item })), catchError(err => of(loadItemByIdFailure({ error: err }))) )) ) }) )); // 监听进入列表页路由,自动加载全量数据 loadListOnRouteEnter$ = createEffect(() => this.actions$.pipe( ofType(ROUTER_NAVIGATION), filter((action: RouterNavigationAction) => action.payload.routerState.url === '/items'), switchMap(() => this.store.pipe( select(selectAllItemsLoaded), take(1), // 已加载过全量数据就跳过,避免重复请求 filter(loaded => !loaded), switchMap(() => this.itemsService.getAll().pipe( map(items => loadItemsSuccess({ items })), catchError(err => of(loadItemsFailure({ error: err }))) )) )) )); constructor( private actions$: Actions, private store: Store<fromReducer.State>, private itemsService: ItemsService ) {} }
这种方案下组件只需要负责从Store选数据、渲染视图,完全不用关心数据加载逻辑,任何入口进入对应路由都会自动触发数据校验和加载,配合状态位标记可以彻底避免重复请求。
方案3:路由Resolver预加载(适合需数据就绪再渲染场景)
如果你的业务要求必须等数据加载完成才允许激活路由、渲染组件,可以使用Angular内置的路由Resolver做预加载:
// item.resolver.ts @Injectable({ providedIn: 'root' }) export class ItemResolver implements Resolve<boolean> { constructor(private store: Store<fromReducer.State>) {} resolve(route: ActivatedRouteSnapshot): Observable<boolean> { const itemId = route.params['id']; return this.store.pipe( select(getItemById(itemId)), take(1), switchMap(existItem => { if (existItem) return of(true); this.store.dispatch(loadItemById({ id: itemId })); // 等待数据加载完成再放行路由 return this.store.pipe( select(getItemById(itemId)), filter(item => !!item), take(1), map(() => true) ); }) ); } } // 路由配置中注册resolver const routes: Routes = [ { path: 'items/:id', component: ItemDetailsComponent, resolve: { dataReady: ItemResolver } } ];
实践注意点
- 所有数据加载逻辑必须加上存在性校验,配合Store中的
loaded、loading状态位拦截重复请求,不要每次进路由都无脑拉取数据。 - 避免把数据加载逻辑和特定组件的实例化强绑定,否则只要组件不被实例化,数据就不会加载,硬导航、多入口跳转场景必然出问题。
内容的提问来源于stack exchange,提问作者EchtFettigerKeks
相关产品推荐
相关产品推荐

