Angular:使用provideAppInitializer与unauthGuard时页面重定向前出现闪烁问题求助
大家好,我最近在Angular项目里遇到了一个路由守卫的小问题,折腾了好一会儿没搞定,想请教下各位的解决方案。
先跟大家说下我的场景:我写了一个unauthGuard,用来限制已登录用户访问/auth/login这类登录相关路由——如果store里的freelancer不为null(说明用户已登录),就直接重定向到首页;如果是null,就允许进入登录页。同时我用provideAppInitializer在应用启动时自动请求用户信息,并把结果dispatch到store里。
先贴一下我的代码:
守卫代码
import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { map } from 'rxjs'; import { AppState } from '../../../shared/store/app.state'; import { selectFreelancer } from '../../../shared/store/freelancer/freelancer.selectors'; export const unauthGuard: CanActivateFn = (route, state) => { const store = inject(Store<AppState>); const router = inject(Router); return store.select(selectFreelancer).pipe( map((freelancer) => { if (freelancer === null) { return true; } router.navigate(['/']); return false; }), ); };
app.config.ts里的初始化逻辑
provideAppInitializer(() => { const store = inject(Store<AppState>); store.dispatch(FreelancerActions.getFreelancer()); return firstValueFrom( store.select(selectOnGetFreelancer).pipe( filter((onGetFreelancer) => ['success', 'error'].includes(onGetFreelancer)), take(1), ), ); }),
现在功能基本能跑,但有个很影响体验的问题:当已登录的用户直接输入/auth/login访问时,登录页面会短暂闪一下,然后才跳转到首页。按道理应该直接跳转,完全看不到登录页才对。
我自己琢磨了下,问题应该出在路由守卫的执行时机比appInitializer的异步请求更早。应用启动时,router先触发守卫,这时候store里的freelancer还是初始值null,所以guard直接允许进入登录页;等appInitializer里的用户信息请求完成,store更新了freelancer数据,guard才检测到用户已登录并执行重定向,这就导致了页面闪烁。
针对这个问题,我整理了几个可行的解决方案,给大家参考:
方案一:让守卫等待用户数据加载完成
核心思路是给store加一个表示用户数据加载状态的selector(比如selectIsFreelancerLoading),让守卫先等加载完成,再判断用户是否已登录。
修改后的守卫代码:
import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { combineLatest, filter, map } from 'rxjs'; import { AppState } from '../../../shared/store/app.state'; import { selectFreelancer, selectIsFreelancerLoading } from '../../../shared/store/freelancer/freelancer.selectors'; export const unauthGuard: CanActivateFn = (route, state) => { const store = inject(Store<AppState>); const router = inject(Router); return combineLatest([ store.select(selectFreelancer), store.select(selectIsFreelancerLoading) ]).pipe( // 等待加载状态结束 filter(([_, isLoading]) => !isLoading), map(([freelancer]) => { if (freelancer === null) { return true; } router.navigate(['/']); return false; }), ); };
这里需要确保你的freelancer state里维护了isLoading字段:触发getFreelancer action时设为true,请求成功/失败后设为false。
方案二:在初始化完成前显示加载状态
另一种思路是在根组件里做判断,直到appInitializer完成用户数据加载,才渲染router-outlet,否则显示加载动画。
比如在app.component.ts里:
import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from './shared/store/app.state'; import { selectOnGetFreelancer } from './shared/store/freelancer/freelancer.selectors'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-root', template: ` @if (isLoading$ | async) { <div class="loading-container">正在加载...</div> } @else { <router-outlet></router-outlet> } `, styles: [` .loading-container { display: flex; justify-content: center; align-items: center; height: 100vh; } `] }) export class AppComponent { private store = inject(Store<AppState>); isLoading$: Observable<boolean> = this.store.select(selectOnGetFreelancer).pipe( map(status => status !== 'success' && status !== 'error') ); }
这样在用户数据加载完成前,用户只会看到加载状态,不会触发路由渲染,自然就不会有页面闪烁的问题。
方案三:用初始状态区分“未加载”和“未登录”
把freelancer的初始值从null改成undefined,用来区分“还没加载用户数据”和“加载完成后确认未登录”两种状态。然后修改守卫逻辑,只有当freelancer明确为null时才允许进入登录页,否则等待加载完成。
修改后的守卫:
export const unauthGuard: CanActivateFn = (route, state) => { const store = inject(Store<AppState>); const router = inject(Router); return store.select(selectFreelancer).pipe( // 等待数据加载完成(排除undefined状态) filter(freelancer => freelancer !== undefined), map((freelancer) => { if (freelancer === null) { return true; } router.navigate(['/']); return false; }), ); };
同时要在freelancer的reducer里调整初始状态:
const initialState = { freelancer: undefined, // 初始值设为undefined isLoading: false, onGetFreelancer: 'idle' };
当getFreelancer请求成功时把freelancer设为用户对象,失败时设为null。
我个人更倾向方案一,因为它直接在守卫层面解决问题,不需要修改根组件的结构,逻辑也比较清晰。大家可以根据自己项目的state结构选择最适合的方案。
内容来源于stack exchange

