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

如何在Angular中创建自定义Service Worker?非PWA场景跨标签页状态管理

跨标签页状态管理方案(无PWA、TypeScript实现)

核心思路

利用浏览器同源标签共享的localStorage作为状态同步载体,结合内存缓存减少IO操作,通过锁机制避免重复API请求,同时监听storage事件实现跨标签状态实时同步,全程不需要依赖PWA或Angular默认的Service Worker。

实现代码

type StateKey = string;
type StateValue = any;
type ApiRequestFn<T = StateValue> = () => Promise<T>;

export class CrossTabStateManager {
  // 内存缓存,快速获取当前状态
  private stateCache = new Map<StateKey, StateValue>();
  // 请求锁,避免同一key的API请求重复发起
  private requestLocks = new Map<StateKey, Promise<StateValue>>();
  // 存储事件监听标识,避免重复绑定
  private isListening = false;

  constructor() {
    this.initStorageListener();
  }

  private initStorageListener(): void {
    if (this.isListening) return;
    window.addEventListener('storage', (e) => {
      if (!e.key || e.newValue === null) return;
      // 同步内存缓存
      const parsedValue = JSON.parse(e.newValue);
      this.stateCache.set(e.key, parsedValue);
      // 触发自定义事件,通知组件状态变更
      window.dispatchEvent(new CustomEvent(`state:${e.key}-updated`, { detail: parsedValue }));
    });
    this.isListening = true;
  }

  /**
   * 获取状态:优先从内存取,内存无则从localStorage取,都没有则调用API获取
   * @param key 状态键
   * @param apiFn 获取状态的API请求函数
   * @param forceRefresh 是否强制刷新(跳过缓存直接调用API)
   */
  async getState<T = StateValue>(key: StateKey, apiFn: ApiRequestFn<T>, forceRefresh = false): Promise<T> {
    // 强制刷新则清空缓存和锁
    if (forceRefresh) {
      this.stateCache.delete(key);
      this.requestLocks.delete(key);
    }

    // 内存有缓存直接返回
    const cachedValue = this.stateCache.get(key);
    if (cachedValue !== undefined && !forceRefresh) {
      return cachedValue as T;
    }

    // 已有请求在进行,直接复用Promise避免竞态
    const existingRequest = this.requestLocks.get(key);
    if (existingRequest) {
      return existingRequest as Promise<T>;
    }

    // 发起新请求,加锁
    const requestPromise = apiFn()
      .then((result) => {
        // 更新内存缓存和localStorage
        this.stateCache.set(key, result);
        localStorage.setItem(key, JSON.stringify(result));
        return result;
      })
      .catch((error) => {
        // 请求失败释放锁
        this.requestLocks.delete(key);
        throw error;
      })
      .finally(() => {
        // 无论成功失败,最终释放锁
        this.requestLocks.delete(key);
      });

    this.requestLocks.set(key, requestPromise);
    return requestPromise as Promise<T>;
  }

  /**
   * 主动更新状态,同步到所有标签页
   * @param key 状态键
   * @param value 新状态值
   */
  setState(key: StateKey, value: StateValue): void {
    this.stateCache.set(key, value);
    localStorage.setItem(key, JSON.stringify(value));
    // 主动触发自定义事件,当前标签页也能收到更新通知
    window.dispatchEvent(new CustomEvent(`state:${key}-updated`, { detail: value }));
  }

  /**
   * 监听指定状态的变更
   * @param key 状态键
   * @param callback 变更回调
   */
  onStateChange<T = StateValue>(key: StateKey, callback: (value: T) => void): () => void {
    const handler = (e: CustomEvent<T>) => callback(e.detail);
    window.addEventListener(`state:${key}-updated`, handler as EventListener);
    // 返回取消监听的函数
    return () => window.removeEventListener(`state:${key}-updated`, handler as EventListener);
  }
}

// 全局单例实例
export const crossTabStateManager = new CrossTabStateManager();

使用示例(Angular环境)

在Angular组件中直接使用全局单例:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { crossTabStateManager } from './cross-tab-state-manager';

@Component({
  selector: 'app-user-profile',
  template: `<div>{{ userProfile | json }}</div>`
})
export class UserProfileComponent implements OnInit, OnDestroy {
  userProfile: any;
  private unsubscribe?: () => void;

  ngOnInit(): void {
    // 获取用户信息:首次调用会发起API,其他标签页直接复用缓存
    crossTabStateManager.getState('userProfile', () => {
      return fetch('/api/user/profile').then(res => res.json());
    }).then(profile => {
      this.userProfile = profile;
    });

    // 监听状态变更,实时更新视图
    this.unsubscribe = crossTabStateManager.onStateChange('userProfile', (profile) => {
      this.userProfile = profile;
    });
  }

  ngOnDestroy(): void {
    this.unsubscribe?.();
  }

  // 主动更新用户信息,同步到所有标签页
  updateProfile(newProfile: any): void {
    crossTabStateManager.setState('userProfile', newProfile);
  }
}

关键特性说明

  • 竞态条件规避:通过requestLocks记录正在进行的API请求,同一状态的重复请求会直接复用已有Promise,避免多个标签页同时发起相同API。
  • 无重复API调用:内存缓存+localStorage双重缓存,只有在缓存不存在或强制刷新时才会调用API。
  • 跨标签同步:利用storage事件监听其他标签的状态变更,同时主动触发自定义事件确保当前标签也能收到更新通知。
  • 轻量无依赖:纯TypeScript实现,不需要PWA、Service Worker或任何第三方状态管理库。

注意事项

  • 存储的状态需要能被JSON序列化,复杂对象需提前处理。
  • 若不需要持久化状态,可将localStorage替换为sessionStorage,关闭标签后状态自动清除。
  • 异常处理:API请求失败时会自动释放锁,避免后续请求被阻塞。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 02:05:35