Angular 7子路由CanActivate先于父路由Resolver执行的问题及解决方法
为什么ManageUsersGuard会在父路由Resolver之前执行?
我先帮你拆解下这个问题的核心原因:
非首次进入子路由的场景:如果你已经处于
dashboard父路由下(比如先访问了dashboard的其他子路由),然后再导航到manage-users,这时候Angular不会重新触发父路由的resolve逻辑——因为父路由已经激活过了。这时候子路由的canActivate会直接执行,你可能会误以为是“守卫先于父resolve执行”,但实际上父resolve早就执行过了,只是你没从正确的地方拿到数据。异步逻辑的调度问题:如果你的
AuthGuard或者UserDataResolver是异步操作(比如返回Observable/Promise),偶尔可能因为RxJS的调度顺序,让你觉得子守卫先跑了,但正常情况下Angular会严格遵循「父canActivate→ 父resolve→ 子canActivate」的顺序执行,除非你的异步逻辑没正确完成(比如Observable没调用complete())。路由快照的误解:你可能以为子路由的
snapshot.data会包含父路由的resolve数据,但实际上子路由的snapshot.data只包含自己的resolve数据,父路由的存在于route.parent.data里。
靠谱的解决方案
方案1:直接从父路由快照获取数据(最简单)
在ManageUsersGuard里,通过子路由快照的parent属性直接拿到父路由已经解析好的userData,根本不需要重复调用API:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class ManageUsersGuard implements CanActivate { constructor(private router: Router) {} canActivate(route: ActivatedRouteSnapshot): boolean { // 从父路由快照拿userData const userData = route.parent?.data?.['userData']; if (!userData) { // 没拿到数据,说明可能没登录,直接跳登录页 this.router.navigate(['/login']); return false; } // 这里写你的权限判断逻辑,比如检查用户角色 return userData.permissions.includes('manage_users'); } }
方案2:把用户数据存到全局服务(最推荐)
这是最稳妥的方式,避免依赖路由快照的不确定性:在UserDataResolver拿到数据后,把它存到一个全局的用户服务里,之后所有需要用的地方(包括守卫)都直接从服务取。
第一步,创建用户服务:
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class UserService { private userData$ = new BehaviorSubject<any>(null); currentUserData$ = this.userData$.asObservable(); setUserData(data: any) { this.userData$.next(data); } getCurrentUserData() { return this.userData$.value; } }
第二步,修改UserDataResolver,把数据存到服务:
import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; import { UserService } from './user.service'; import { UserApiService } from './user-api.service'; @Injectable({ providedIn: 'root' }) export class UserDataResolver implements Resolve<any> { constructor(private userApi: UserApiService, private userService: UserService) {} resolve() { return this.userApi.getUserData().then(data => { this.userService.setUserData(data); // 存到服务 return data; }); } }
第三步,修改ManageUsersGuard,从服务拿数据:
import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { UserService } from './user.service'; import { map, take } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ManageUsersGuard implements CanActivate { constructor(private userService: UserService, private router: Router) {} canActivate() { return this.userService.currentUserData$.pipe( take(1), // 只取一次值,避免内存泄漏 map(userData => { if (!userData) { this.router.navigate(['/login']); return false; } return userData.permissions.includes('manage_users'); }) ); } }
方案3:调整路由复用策略(针对父路由数据丢失的情况)
如果是从父路由内部跳转子路由时,父路由的data丢失了,可以自定义路由复用策略,让Angular保留父路由的状态:
import { RouteReuseStrategy, DetachedRouteHandle, ActivatedRouteSnapshot } from '@angular/router'; export class CustomRouteReuseStrategy implements RouteReuseStrategy { shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { // 对于dashboard路由,允许复用其状态 return future.routeConfig === curr.routeConfig || future.parent?.routeConfig?.path === 'dashboard'; } retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { return null; } store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {} shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; } shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; } }
然后在AppModule里注册这个策略:
import { NgModule } from '@angular/core'; import { RouteReuseStrategy } from '@angular/router'; import { CustomRouteReuseStrategy } from './custom-route-reuse-strategy'; @NgModule({ providers: [ { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy } ] }) export class AppModule {}
内容的提问来源于stack exchange,提问作者Ben Beri

