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

Angular多浏览器窗口下@ng-idle/core定时器卡顿冻结问题求助

解决方案

问题根源分析

  • 多窗口倒计时冲突:每个窗口独立启动setInterval,同时修改localStorage中的倒计时数值引发竞态条件;点击"保持登录"后仅当前窗口清除定时器,其他窗口的定时器仍在运行,导致数值混乱。
  • @ng-idle配置错误:代码中idle.setIdle(60)设置为1分钟无活动进入空闲,与需求的20分钟不符,提前触发逻辑导致异常。
  • 性能与节流问题:频繁读写localStorage+setInterval在后台标签页被浏览器节流,引发窗口冻结或倒计时卡顿。

具体修复步骤

1. 修正@ng-idle基础配置

将空闲触发时间改为20分钟(1200秒),匹配需求:

// 原错误配置
// idle.setIdle(60);
// 修改为
idle.setIdle(1200); // 20分钟无活动进入空闲
idle.setTimeout(60); // 空闲后60秒倒计时登出

2. 重构多窗口同步逻辑

  • 存储倒计时结束时间戳而非剩余秒数,减少localStorage读写次数
  • 用窗口唯一标识标记主导倒计时的窗口,避免多窗口重复启动定时器
  • 通过storage事件同步所有窗口的状态

3. 优化定时器实现

用requestAnimationFrame替代setInterval,适配浏览器后台节流机制,避免定时器累积导致的性能问题。


修改后的完整代码示例

import { ChangeDetectorRef, Component, OnInit, OnDestroy } from "@angular/core";
import { MenuItem } from "primeng/api";
import { LoginService } from "./components/login/login.service";
import { Idle, DEFAULT_INTERRUPTSOURCES } from "@ng-idle/core";
import { DialogService } from "primeng/dynamicdialog";
import { Keepalive } from "@ng-idle/keepalive";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  providers: [DialogService],
})
export class AppComponent implements OnInit, OnDestroy {
  items: MenuItem[];
  title = "maincontainer";

  idleState = "NOT_STARTED";
  countdown?: number = null;
  lastPing?: Date = null;
  showAutoLogout: boolean = false;
  showSpinner: boolean = false;
  private countdownTimer: any;
  private windowId: string = Math.random().toString(36).substring(2, 15); // 当前窗口唯一标识

  constructor(
    public loginService: LoginService,
    private idle: Idle,
    private keepalive: Keepalive,
    private cd: ChangeDetectorRef
  ) {
    // 初始化localStorage状态
    if (!localStorage.getItem('countdownEndTime')) localStorage.setItem('countdownEndTime', '0');
    if (!localStorage.getItem('isCounting')) localStorage.setItem('isCounting', 'false');

    // 修正空闲配置:20分钟无活动进入空闲,空闲后60秒倒计时
    idle.setIdle(1200);
    idle.setTimeout(60);
    idle.setInterrupts(DEFAULT_INTERRUPTSOURCES);

    idle.onIdleStart.subscribe(() => {
      this.idleState = "IDLE";
      idle.clearInterrupts();
      this.checkLogin();
    });

    idle.onIdleEnd.subscribe(() => {
      if (localStorage.getItem("bearerToken")) {
        this.idleState = "NOT_IDLE";
        this.reset();
      } else {
        this.logout();
      }
    });

    idle.onTimeout.subscribe(() => {
      this.idleState = "TIMED_OUT";
      this.countdown = null;
      this.showAutoLogout = false;
      this.logout();
    });

    idle.onTimeoutWarning.subscribe(() => {
      this.checkLogin();
      this.showAutoLogout = true;
      // 仅当无窗口在倒计时时,当前窗口启动倒计时
      if (localStorage.getItem('isCounting') === "false") this.startCountdown();
    });

    keepalive.onPing.subscribe(() => {
      this.lastPing = new Date();
    });
  }

  ngOnInit(): void {
    // 监听localStorage变化,同步所有窗口状态
    window.addEventListener('storage', (event) => {
      if (event.key === 'countdownEndTime' || event.key === 'isCounting') {
        this.updateCountdownDisplay();
      } else if (event.key === "loggedIn") {
        localStorage.getItem("loggedIn") === 'true' ? window.location.reload() : window.location.href = '/login';
      }
    });

    if (this.loginService.isLoggedIn()) this.reset();

    this.loginService.userLoggedIn.subscribe(() => this.reset());

    this.loginService.userLoggedOut.subscribe(() => {
      localStorage.clear();
      window.localStorage.setItem('loggedIn', 'false');
      window.location.reload();
    });

    // 初始化显示倒计时
    this.updateCountdownDisplay();
  }

  ngOnDestroy(): void {
    // 组件销毁时清除定时器并重置状态
    this.clearCountdownTimer();
    if (localStorage.getItem('countdownOwner') === this.windowId) {
      localStorage.setItem('isCounting', 'false');
      localStorage.setItem('countdownOwner', '');
    }
  }

  private startCountdown(): void {
    // 设置倒计时结束时间:当前时间+60秒
    const endTime = Date.now() + 60 * 1000;
    localStorage.setItem('countdownEndTime', endTime.toString());
    localStorage.setItem('isCounting', 'true');
    localStorage.setItem('countdownOwner', this.windowId);

    // 使用requestAnimationFrame实现倒计时,适配浏览器节流
    const updateCountdown = () => {
      const now = Date.now();
      const remaining = Math.max(0, Math.floor((+localStorage.getItem('countdownEndTime')! - now) / 1000));
      
      this.countdown = remaining;
      this.cd.detectChanges();

      if (remaining <= 0) {
        this.showAutoLogout = false;
        this.logout();
        this.clearCountdownTimer();
        localStorage.setItem('isCounting', 'false');
        localStorage.setItem('countdownOwner', '');
      } else {
        this.countdownTimer = requestAnimationFrame(updateCountdown);
      }
    };

    this.clearCountdownTimer();
    updateCountdown();
  }

  private updateCountdownDisplay(): void {
    if (localStorage.getItem('isCounting') === "true") {
      const now = Date.now();
      const remaining = Math.max(0, Math.floor((+localStorage.getItem('countdownEndTime')! - now) / 1000));
      this.countdown = remaining;
      this.showAutoLogout = true;
      this.cd.detectChanges();
    } else {
      this.countdown = null;
      this.showAutoLogout = false;
    }
  }

  private clearCountdownTimer(): void {
    if (this.countdownTimer) {
      cancelAnimationFrame(this.countdownTimer);
      this.countdownTimer = null;
    }
  }

  checkLogin(): void {
    if (!this.loginService.isLoggedIn()) {
      this.idle.stop();
      window.location.href = "/login";
    }
  }

  reset(): void {
    // 重置所有倒计时状态
    this.clearCountdownTimer();
    localStorage.setItem('isCounting', 'false');
    localStorage.setItem('countdownEndTime', '0');
    localStorage.setItem('countdownOwner', '');
    
    this.idle.stop();
    this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES);
    this.idleState = "NOT_IDLE";
    this.cd.detectChanges();
    this.idle.watch();
    this.countdown = null;
    this.lastPing = null;
    this.showAutoLogout = false;
  }

  logout(): void {
    this.showSpinner = true;
    const token = localStorage.getItem("bearerToken");
    const logoutData = {
      loginId: localStorage.getItem("loginId"),
      loginReportId: localStorage.getItem("loginReportId"),
    };

    if (!this.loginService.isLoggedIn()) {
      this.loginService.userLoggedOut.emit();
      return;
    }

    this.loginService.logout(logoutData, token).subscribe(
      () => {
        this.showSpinner = false;
        this.loginService.userLoggedOut.emit();
        this.idle.stop();
      },
      () => {
        this.showSpinner = false;
        this.loginService.userLoggedOut.emit();
        this.idle.stop();
      }
    );
  }
}

关键修改说明

  • 倒计时逻辑重构:通过结束时间戳计算剩余秒数,大幅减少localStorage读写操作,避免竞态条件。
  • 多窗口协作:用windowId标记主导倒计时的窗口,配合storage事件同步所有窗口的显示状态。
  • 性能优化:requestAnimationFrame适配浏览器后台节流,避免定时器累积导致的窗口冻结;组件销毁时清理定时器,防止内存泄漏。
  • 配置修正:将空闲触发时间改为20分钟,完全匹配需求。

内容的提问来源于stack exchange,提问作者Saurin Shah

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 05:30:19