解决Angular 19登录后菜单闪烁问题(angular-auth-oidc-client)
解决Angular 19 + angular-auth-oidc-client的菜单闪烁问题
问题背景
使用Angular 19搭配angular-auth-oidc-client开发时,登录完成后或刷新已登录状态的页面,会短暂显示登录按钮(菜单闪烁),相关代码如下:
HomeComponent TS代码
export class HomeComponent implements OnInit { private readonly oidcSecurityService = inject(OidcSecurityService); private readonly eventService = inject(PublicEventsService); isAuthenticated = signal<boolean>(false); isCheckingAuth = signal<boolean>(true); ngOnInit(): void { this.oidcSecurityService.isAuthenticated$.subscribe(({ isAuthenticated }) => { this.isAuthenticated.set(isAuthenticated); this.isCheckingAuth.set(false); }); this.eventService .registerForEvents() .pipe(filter((event) => event.type === EventTypes.CheckSessionReceived || event.type === EventTypes.UserDataChanged)) .subscribe((event) => { console.log('Auth event received:', event); this.oidcSecurityService.isAuthenticated$.subscribe(({ isAuthenticated }) => { this.isAuthenticated.set(isAuthenticated); }); }); } }
HTML模板代码
<div *ngIf="!isCheckingAuth(); else loading"> <div *ngIf="isAuthenticated(); else noAuth"> <button (click)="logout()">Logout</button> <button (click)="logoffAndRevokeTokens()">Logout and revoke tokens</button> Is Authenticated: {{ isAuthenticated() }} </div> </div> <ng-template #noAuth> <button (click)="login()">Login</button> <hr /> </ng-template> <ng-template #loading> <p>Loading authentication...</p> </ng-template>
问题原因
- 初始时
isAuthenticated信号默认设为false,而isCheckingAuth要等到isAuthenticated$第一次回调才会设为false,这中间的时间差会让noAuth模板(登录按钮)短暂显示。 - 事件订阅中嵌套
isAuthenticated$的订阅,不仅容易造成内存泄漏,还可能导致状态更新延迟。
解决方案
方案一:同步获取初始认证状态,优化订阅逻辑
修改TS代码,提前获取当前认证状态,避免初始默认值导致的闪烁,同时清理嵌套订阅:
import { firstValueFrom, Subject, takeUntil } from 'rxjs'; import { EventTypes } from 'angular-auth-oidc-client'; import { OnInit, OnDestroy } from '@angular/core'; export class HomeComponent implements OnInit, OnDestroy { private readonly oidcSecurityService = inject(OidcSecurityService); private readonly eventService = inject(PublicEventsService); private destroy$ = new Subject<void>(); isAuthenticated = signal<boolean>(false); isCheckingAuth = signal<boolean>(true); async ngOnInit(): Promise<void> { // 同步获取当前认证状态,立即更新信号 const authState = await firstValueFrom(this.oidcSecurityService.isAuthenticated$); this.isAuthenticated.set(authState.isAuthenticated); this.isCheckingAuth.set(false); // 订阅后续认证状态变化,用takeUntil防止内存泄漏 this.oidcSecurityService.isAuthenticated$ .pipe(takeUntil(this.destroy$)) .subscribe(({ isAuthenticated }) => { this.isAuthenticated.set(isAuthenticated); }); // 处理认证事件,直接调用服务获取最新状态 this.eventService.registerForEvents() .pipe( takeUntil(this.destroy$), filter(event => event.type === EventTypes.CheckSessionReceived || event.type === EventTypes.UserDataChanged ) ) .subscribe(() => { this.oidcSecurityService.getIsAuthenticated().then(isAuthenticated => { this.isAuthenticated.set(isAuthenticated); }); }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } // 补充登录登出方法 login(): void { this.oidcSecurityService.authorize(); } logout(): void { this.oidcSecurityService.logoff(); } logoffAndRevokeTokens(): void { this.oidcSecurityService.logoffAndRevokeTokens(); } }
方案二:使用toSignal简化Observable转信号(Angular 19+推荐)
利用Angular 19的toSignal将isAuthenticated$转为信号,避免手动订阅和初始默认值问题:
import { toSignal, effect } from '@angular/core/rxjs-interop'; import { Subject, takeUntil } from 'rxjs'; import { EventTypes } from 'angular-auth-oidc-client'; import { OnInit, OnDestroy } from '@angular/core'; export class HomeComponent implements OnInit, OnDestroy { private readonly oidcSecurityService = inject(OidcSecurityService); private readonly eventService = inject(PublicEventsService); private destroy$ = new Subject<void>(); // 将Observable转为信号,初始值设为null,避免默认false isAuthenticated = toSignal(this.oidcSecurityService.isAuthenticated$, { initialValue: null, }); isCheckingAuth = signal<boolean>(true); ngOnInit(): void { // 监听信号变化,认证状态确定后关闭加载状态 effect(() => { const authState = this.isAuthenticated(); if (authState !== null) { this.isCheckingAuth.set(false); } }); // 处理认证事件,触发服务检查状态,信号自动同步 this.eventService.registerForEvents() .pipe( takeUntil(this.destroy$), filter(event => event.type === EventTypes.CheckSessionReceived || event.type === EventTypes.UserDataChanged ) ) .subscribe(() => { this.oidcSecurityService.checkAuth(); }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } // 登录登出方法同上 }
对应的HTML模板调整(适配方案二):
<!-- 优先显示加载状态,直到认证状态完全确定 --> <ng-container *ngIf="isCheckingAuth(); else authContent"> <p>Loading authentication...</p> </ng-container> <ng-template #authContent> <div *ngIf="isAuthenticated()?.isAuthenticated; else noAuth"> <button (click)="logout()">Logout</button> <button (click)="logoffAndRevokeTokens()">Logout and revoke tokens</button> <p>Is Authenticated: {{ isAuthenticated()?.isAuthenticated }}</p> </div> <ng-template #noAuth> <button (click)="login()">Login</button> <hr /> </ng-template> </ng-template>
通用模板优化
无论使用哪种方案,都要确保加载状态的优先级高于认证内容,避免在认证检查完成前显示错误的菜单内容。
内容的提问来源于stack exchange,提问作者Miguel Moura
相关产品推荐
相关产品推荐

