Angular路由守卫如何等待API加载权限数据后再执行?
我给Angular路由配置了路由守卫,希望在守卫返回true/false前先从API加载侧边栏菜单数据。正常跳转时功能正常,但刷新页面时,守卫会因为DropdownUtils的初始值是空对象直接返回false,没等API加载完成。
相关代码
app-routing文件
{ path: '', component: BaseLayoutComponent, runGuardsAndResolvers: 'always', children:[ { path: 'main' , resolve:{ menu: MenuResolver }, loadChildren: () => import('./main/main.module').then(m => m.MainModule), canActivate: [AuthGuard] }, ] },
main-routing文件
{ path: '', component: MainComponent, children: [ { path: 'user', loadChildren: () => import('./users-management/users-management.module').then(m => m.UsersManagementModule), canActivate: [MainGuard], }, ] }
MenuResolver
export class MenuResolver implements Resolve<any> { constructor(private dropDownUtils: DropDownUtils) {} resolve(): Observable<any> { return this.dropDownUtils.updatePermission(); } }
DropDownUtils类
export class DropDownUtils { protected permissionsSubject = new BehaviorSubject<any>({}); public permissions$: Observable<any> = this.permissionsSubject.asObservable(); currentUrl: any; constructor(private menuService: MenuService, private router: Router) { this.router.events.pipe( filter(event => event instanceof NavigationEnd) ).subscribe(() => { this.fetchPermissions().subscribe(); }); } public updatePermission(): Observable<any> { return this.fetchPermissions(); } private fetchPermissions(): Observable<any> { return this.menuService.getUserMenu().pipe( tap((data: any) => { this.extractPermissions(data.data); }) ); } private extractPermissions(menu: any[]): void { const checkPermissions: any[] = []; console.log(this.router.url) if (menu.length > 0) { menu.forEach(element => { const screen = element.screens.find((x: any) => x.url === this.router.url); if (screen) { checkPermissions.push(screen); } }); } if (checkPermissions.length > 0) { const permissions = { is_create: checkPermissions.some(item => item.is_create), is_delete: checkPermissions.some(item => item.is_delete), is_read: checkPermissions.some(item => item.is_read), is_update: checkPermissions.some(item => item.is_update) }; this.permissionsSubject.next(permissions); } } }
MainGuard的canActivate方法
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { console.log('Guard is activated'); window.scrollTo(0, 0); const token = TokenHelper.getAccessToken(); if (!token) { console.log('Token is missing or expired'); this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return of(false); } if (this.jwtService.isTokenExpired(token)) { console.log('Token is expired.'); this.authService.logout(); return of(false); } return this.menu.resolve().pipe( switchMap(() => { console.log('Menu is resolved'); return this.dropDownUtils.updatePermission().pipe( switchMap(() => this.dropDownUtils.permissions$), map((data: any) => { console.log('Permissions data received', data); const hasPermission = data.is_create || data.is_delete || data.is_read || data.is_update; if (hasPermission) { console.log('User has at least one required permission:', hasPermission); return true; } console.log('User lacks the necessary permissions'); this.router.navigate(['403']); return false; }), catchError(error => { console.error('Error in permissions pipeline:', error); this.router.navigate(['403']); return of(false); }) ); }), catchError(error => { console.error('Error resolving menu:', error); this.router.navigate(['403']); return of(false); }) ); }
用户组件获取权限的方式
ngOnInit() { this.dropdownUtils.permissions$ .pipe( takeUntil(this.destroy$), finalize(() => {}) ) .subscribe((permissions:any) => { this.permission = permissions; console.log(this.permission) this.loadData(this.currentPage, this.rowsPerPage); }); }
问题分析与修复方案
当前实现的问题
刷新页面时,MainGuard里的switchMap(() => this.dropDownUtils.permissions$)会直接取到BehaviorSubject的初始空对象,因为fetchPermissions的API请求还没完成,导致权限校验直接失败返回false。另外,MenuResolver已经调用了updatePermission,但MainGuard里又重复调用一次,属于冗余操作,还可能引发时序问题。
修复步骤
调整BehaviorSubject初始值
将初始空对象改为null,区分“未加载”和“无权限”状态:protected permissionsSubject = new BehaviorSubject<any>(null);过滤守卫中的初始未加载状态
在订阅permissions$时,用filter跳过初始null值,确保只处理API加载后的有效数据:return this.dropDownUtils.updatePermission().pipe( switchMap(() => this.dropDownUtils.permissions$.pipe( filter(data => data !== null) )), map((data: any) => { // 原权限校验逻辑不变 }) );移除重复的权限请求
利用MenuResolver已加载的菜单数据,避免重复调用updatePermission:// 从父路由获取Resolver返回的菜单数据 const menuData = route.parent?.data['menu']; if (menuData) { return this.dropDownUtils.permissions$.pipe( filter(data => data !== null), map(data => { // 权限校验逻辑 }) ); }完善权限发送逻辑
确保无论是否找到对应路由的权限,都发送明确的权限对象,避免守卫无限等待:private extractPermissions(menu: any[]): void { const checkPermissions: any[] = []; // 原路由匹配逻辑不变... const permissions = checkPermissions.length > 0 ? { is_create: checkPermissions.some(item => item.is_create), is_delete: checkPermissions.some(item => item.is_delete), is_read: checkPermissions.some(item => item.is_read), is_update: checkPermissions.some(item => item.is_update) } : { is_create: false, is_delete: false, is_read: false, is_update: false }; this.permissionsSubject.next(permissions); }
是否需要用NgRx替代RxJS?
如果只是解决当前的权限加载时序问题,不需要立刻切换到NgRx,上述RxJS调整已经能解决问题。但如果项目存在多处全局状态共享、复杂数据流管理场景,NgRx可以提供更规范的状态管理方案,让权限这类全局状态的维护更清晰。简单场景下,保持现有RxJS实现足够轻量高效。
内容的提问来源于stack exchange,提问作者Muhammad Arslan

