Angular子路由守卫导致父路由无法加载问题求助
我配置了如下路由:
const routes: Routes = [ { path: '', component: ConfigManagerComponent, canActivate: [authGuard], children: [ { path: ':configName', component: ConfigDetailsComponent, canActivate: [configValidationGuard] }, ... ] } ];
对应的守卫代码:
export const configValidationGuard = () => { const store = inject(Store); const api = inject(ConfigApiService); const router = inject(Router); return store.select(selectActiveConfig).pipe( withLatestFrom(api.isConfigsLoaded$), filter(([_, isLoaded]) => isLoaded), map(config => { if (!config) return router.createUrlTree(['no-config']); return true; }) ); };
守卫的核心作用是当configName不正确时,阻止ConfigDetailsComponent加载。selectActiveConfig用于从store中根据名称选取配置,但使用该选择器前需要先填充store数据,因此引入了isLoaded标识——在ConfigManagerComponent中dispatch loadConfigsRequest动作后,会将isLoaded设为true。
对应的Effect代码:
loadConfigs$ = createEffect(() => { return this.actions$.pipe( ofType(configsActions.loadConfigsRequest), switchMap(() => this.api.getAllConfigs().pipe( map(configs => configsActions.loadConfigsSuccess({ configs })), tap(() => this.api.setConfigsLoaded()), catchError(err => of(configsActions.loadConfigsFail({ err }))) ) ) ); });
Service代码:
export class ConfigApiService { private isConfigsLoadedSubject$ = new BehaviorSubject<boolean>(false); isConfigsLoaded$ = this.isConfigsLoadedSubject$ as Observable<boolean>; setConfigsLoaded() { this.isConfigsLoadedSubject$.next(true); ..... } }
添加configValidationGuard后,ConfigManagerComponent甚至无法被实例化;移除该守卫后一切正常。尝试改用Resolver守卫,结果仍然相同。
问题出在守卫的执行时机:Angular会在激活子路由之前先执行子路由的守卫,但此时父组件ConfigManagerComponent还未实例化,也就没有dispatch loadConfigsRequest动作,导致isLoaded$一直处于false状态,守卫的filter操作会一直阻塞流,路由激活流程被卡住,父组件自然也无法完成实例化。
简单来说:子路由守卫先执行了,但父组件还没机会触发加载配置的动作,守卫一直在等isLoaded变为true,整个路由激活流程直接卡死。
方案1:提前加载配置(推荐)
把配置加载逻辑移到父路由守卫或应用初始化阶段,确保子路由守卫执行前数据已准备好。
方法A:父路由守卫中加载配置
修改父路由的authGuard,在认证通过后触发配置加载,完成后再放行:
export const authGuard = () => { const store = inject(Store); const api = inject(ConfigApiService); // 原有认证逻辑 return authCheck$.pipe( switchMap(isAuthenticated => { if (isAuthenticated) { // 先加载配置,再放行 return store.dispatch(configsActions.loadConfigsRequest()).pipe( switchMap(() => api.isConfigsLoaded$), filter(isLoaded => isLoaded), map(() => true) ); } return of(false); }) ); };
方法B:应用启动时加载配置
在app.module.ts中配置APP_INITIALIZER,让应用启动时就加载配置:
import { APP_INITIALIZER } from '@angular/core'; import { ConfigApiService } from './path-to-config-api.service'; import { Store } from '@ngrx/store'; import { configsActions } from './path-to-configs-actions'; export function loadConfigsOnInit(store: Store, api: ConfigApiService) { return () => store.dispatch(configsActions.loadConfigsRequest()).pipe( switchMap(() => api.isConfigsLoaded$), filter(isLoaded => isLoaded) ).toPromise(); } @NgModule({ providers: [ { provide: APP_INITIALIZER, useFactory: loadConfigsOnInit, deps: [Store, ConfigApiService], multi: true } ] }) export class AppModule { }
方案2:修改守卫逻辑,允许初始状态放行
如果不想提前加载配置,可修改守卫,当isLoaded为false时直接放行,让父组件有机会实例化并触发加载,之后再校验:
export const configValidationGuard = () => { const store = inject(Store); const api = inject(ConfigApiService); const router = inject(Router); return store.select(selectActiveConfig).pipe( withLatestFrom(api.isConfigsLoaded$), map(([config, isLoaded]) => { // 未加载完成时先放行,让父组件触发加载 if (!isLoaded) return true; // 加载完成后校验配置是否存在 if (!config) return router.createUrlTree(['no-config']); return true; }) ); };
注意:这种方案可能导致ConfigDetailsComponent短暂加载后跳转,需要在组件中添加加载状态提示。
方案3:守卫内部触发配置加载
直接在守卫中处理配置加载逻辑,不依赖父组件:
export const configValidationGuard = () => { const store = inject(Store); const api = inject(ConfigApiService); const router = inject(Router); return api.isConfigsLoaded$.pipe( switchMap(isLoaded => { if (!isLoaded) { // 未加载则触发加载,直到完成 return store.dispatch(configsActions.loadConfigsRequest()).pipe( switchMap(() => api.isConfigsLoaded$), filter(loaded => loaded) ); } return of(isLoaded); }), switchMap(() => store.select(selectActiveConfig)), map(config => { if (!config) return router.createUrlTree(['no-config']); return true; }) ); };
内容的提问来源于stack exchange,提问作者Oleksii Yurchenko

