如何在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
相关产品推荐
相关产品推荐

