You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决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>

问题原因

  1. 初始时isAuthenticated信号默认设为false,而isCheckingAuth要等到isAuthenticated$第一次回调才会设为false,这中间的时间差会让noAuth模板(登录按钮)短暂显示。
  2. 事件订阅中嵌套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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:55:57