跨浏览器 Tab 通信工具-emit/on 风格 API(仿 mitt)
文章目录
- 前言
- ⚡ 带事件名的 `emit/on` 风格 API(仿 mitt)
- ✅ 功能:
- 📁 文件结构
- 🧩 1. `utils/crossTabEmitter.ts`
- 🧩 2. `composables/useCrossTabEmitter.ts`
- ✅ 3. 使用示例
- 发送事件
- 接收事件
- 🎯 用途示例
- ✅ Bonus:自动管理 + 支持多个频道 + 支持 fallback,几乎所有浏览器都能用。
前言
封装一个前端跨浏览器 Tab 通信工具
⚡ 带事件名的 emit/on
风格 API(仿 mitt)
✅ 功能:
emit(eventName, data)
—— 发送带事件名的消息on(eventName, handler)
—— 添加监听器off(eventName, handler)
—— 移除监听器- 自动使用
BroadcastChannel
(支持同源标签页) - 不支持时自动 fallback 到
localStorage
触发 - Vue 组件中使用时支持
onBeforeUnmount
自动解绑
📁 文件结构
src/
└── utils/
└── crossTabEmitter.ts # 工具核心
└── composables/
└── useCrossTabEmitter.ts # Vue composable 包装
🧩 1. utils/crossTabEmitter.ts
type Handler = (data: any) => void;
interface EventMap {
[eventName: string]: Handler[];
}
export class CrossTabEmitter {
private channelName: string;
private bc: BroadcastChannel | null = null;
private handlers: EventMap = {};
private fallbackKey: string;
constructor(channelName: string) {
this.channelName = channelName;
this.fallbackKey = `__cross_tab_event__${channelName}`;
if ('BroadcastChannel' in window) {
this.bc = new BroadcastChannel(channelName);
this.bc.onmessage = (e) => this._dispatch(e.data);
} else {
window.addEventListener('storage', this._handleStorageEvent);
}
}
private _handleStorageEvent = (e: StorageEvent) => {
if (e.key === this.fallbackKey && e.newValue) {
try {
const { event, data } = JSON.parse(e.newValue);
this._dispatch({ event, data });
} catch {}
}
};
private _dispatch({ event, data }: { event: string; data: any }) {
this.handlers[event]?.forEach((h) => h(data));
}
emit(event: string, data: any) {
const payload = { event, data };
if (this.bc) {
this.bc.postMessage(payload);
} else {
localStorage.setItem(this.fallbackKey, JSON.stringify(payload));
localStorage.removeItem(this.fallbackKey);
}
}
on(event: string, handler: Handler) {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event].push(handler);
}
off(event: string, handler: Handler) {
this.handlers[event] = (this.handlers[event] || []).filter((h) => h !== handler);
}
destroy() {
if (this.bc) {
this.bc.close();
} else {
window.removeEventListener('storage', this._handleStorageEvent);
}
this.handlers = {};
}
}
🧩 2. composables/useCrossTabEmitter.ts
import { onBeforeUnmount } from 'vue';
import { CrossTabEmitter } from '@/utils/crossTabEmitter';
const emittersMap: Record<string, CrossTabEmitter> = {};
export function useCrossTabEmitter(channelName = 'global') {
const emitter = emittersMap[channelName] ||= new CrossTabEmitter(channelName);
function emit(event: string, data: any) {
emitter.emit(event, data);
}
function on(event: string, handler: (data: any) => void) {
emitter.on(event, handler);
onBeforeUnmount(() => {
emitter.off(event, handler);
});
}
return {
emit,
on,
off: emitter.off.bind(emitter),
};
}
✅ 3. 使用示例
- 一个标签页退出登录后,通知其他标签页同时退出登录,清除缓存等。
发送事件
const { emit } = useCrossTabEmitter('auth');
emit('logout', { user: 'admin' });
接收事件
const { on } = useCrossTabEmitter('auth');
on('logout', (payload) => {
console.log('登出事件收到!', payload);
// 做登出清理等操作
});
🎯 用途示例
场景 | 事件名 | 数据结构 |
---|---|---|
用户登出通知 | 'logout' | { user: 'admin' } |
多标签清除缓存 | 'clear-cache' | { area: 'session' } |
登录后刷新页面 | 'login-success' | { token: 'xxx' } |