javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript浏览器不同标签页通信

JavaScript实现浏览器不同标签页通信的原理与实践

作者:Luckily_BAI

这篇文章主要为大家详细系统梳理了不同页签(Tab / Window / Frame)之间的通信手段以及具体的实现方法,感兴趣的小伙伴可以跟随小编一起学习一下

系统梳理不同页签(Tab / Window / Frame)之间的通信手段:能做什么、怎么做、底层原理、限制与踩坑、降级方案,以及一份可落地的通用消息总线实现。

1. 典型场景与选择指南

诉求推荐优先级说明
同源多标签页简单广播BroadcastChannel → localStorage(storage 事件)现代浏览器首选 BroadcastChannel;需要兼容老浏览器时降级到 storage 事件
父子窗口/打开者-被打开者直接通信postMessage + Window 引用通过 window.open、window.opener、iframe.contentWindow 获取引用,再 postMessage
需要单点中枢、复杂路由或持久化SharedWorker / Service WorkerSharedWorker 做内存中枢;Service Worker 能向所有受控客户端群发,并可持久化
跨设备或登录用户维度的广播WebSocket / SSE / WebRTC通过后端转发(或点对点)实现端到端同步
单例任务/互斥锁(而非消息)Web Locks API(navigator.locks)不是通信,但常与通信结合做“选主/互斥”

原则:能用 BroadcastChannel 就别用 localStorage(更干净、性能好、不阻塞)。要兼容老环境,再做降级;有复杂编排再上 Worker。

2. BroadcastChannel:同源广播的首选

2.1 核心 API

// 建立频道
const bc = new BroadcastChannel('app_bus');
​
// 接收
bc.onmessage = (event) => {
  // event.data 支持 Structured Clone(可传对象、ArrayBuffer、MessagePort 等)
  console.log('[BC] recv:', event.data);
};
​
// 发送
bc.postMessage({ type: 'PING', ts: Date.now() });
​
// 关闭
bc.close();

2.2 原理与语义

2.3 实战要点

3. localStorage + storage 事件:兼容兜底

3.1 使用示例

// 监听(只在“其他”标签页触发)
window.addEventListener('storage', (e) => {
  if (e.key === 'app_bus') {
    const payload = JSON.parse(e.newValue || 'null');
    if (payload) handleMessage(payload);
  }
});
​
// 发送(会同步写磁盘;同页不触发 storage 事件)
function send(msg) {
  localStorage.setItem('app_bus', JSON.stringify({
    ...msg,
    id: crypto.randomUUID(),
    ts: Date.now(),
  }));
}

3.2 限制与坑

适用于“偶发广播 + 低频消息 + 老浏览器兼容”。

4. postMessage + Window 引用:点对点直连

4.1 适用场景

4.2 使用示例

// 打开子窗口并握手
const child = window.open('/child.html', 'child');
​
window.addEventListener('message', (event) => {
  // 严格校验来源
  if (event.origin !== location.origin) return;
  console.log('from child:', event.data);
});
​
// 向子窗口发送
child?.postMessage({ type: 'PING' }, location.origin);

跨源时:必须使用精确的 targetOrigin(不要用 *),并对 event.origin 做白名单校验,防止 XSS/点击劫持类问题。

5. SharedWorker:内存中枢

5.1 思路

多个同源页面连接到同一个 SharedWorker,通过 MessagePort 与之通信;Worker 内部充当“Hub”做路由/广播/状态管理。

5.2 示例

worker.js

const ports = new Set();
​
onconnect = (e) => {
  const port = e.ports[0];
  ports.add(port);
​
  port.onmessage = (evt) => {
    // 广播给其他端
    for (const p of ports) {
      if (p !== port) p.postMessage(evt.data);
    }
  };
​
  port.start();
​
  port.addEventListener('close', () => ports.delete(port));
};

页面:

const sw = new SharedWorker('/worker.js');
const port = sw.port;
port.start();
​
port.onmessage = (e) => console.log('[SW] recv:', e.data);
port.postMessage({ type: 'HELLO' });

5.3 优缺点

6. Service Worker:全客户端中转与持久化

6.1 模式

6.2 示例

sw.js

self.addEventListener('message', (event) => {
  // 简单群发
  self.clients.matchAll({ includeUncontrolled: true, type: 'window' })
    .then((clients) => {
      clients.forEach((client) => client.postMessage(event.data));
    });
});
​
self.addEventListener('activate', (e) => self.clients.claim());

页面:

navigator.serviceWorker.register('/sw.js');
​
navigator.serviceWorker.addEventListener('message', (e) => {
  console.log('[SW] recv:', e.data);
});
​
function sendViaSW(data) {
  navigator.serviceWorker.ready.then((reg) => {
    reg.active?.postMessage(data);
  });
}

6.3 要点

7. 服务器中转(WebSocket / SSE / WebRTC)

实战建议:本地(BC/Worker)优先、服务器兜底。对“必须送达”的关键消息,做ACK + 重试

8. 更高阶:SharedArrayBuffer(SAB)与跨上下文共享

跨源隔离(COOP+COEP)启用时,可通过 BroadcastChannel / postMessage 传递 SharedArrayBuffer,配合 Atomics 做无锁队列/环形缓冲,获得极低延迟。

复杂且对环境要求高,通常用于多媒体/计算密集型场景。

9. 通用“可靠广播”模式(ACK/去重/重试)

9.1 消息格式

interface BusMessage<T=any> {
  id: string;           // 唯一 ID(UUID v4)
  ts: number;           // 发送时间戳
  type: string;         // 主题/事件名
  payload: T;           // 载荷
  ack?: boolean;        // 是否为 ACK 消息
  to?: string;          // 指定接收者(可选)
  from?: string;        // 发送者实例 ID
}

9.2 接收侧去重

维护 seenIds(如 LRU 缓存 1–5 分钟),收到重复 id 直接丢弃。

9.3 ACK + 重试

发送后 setTimeout 等待 ACK;超时重发(指数退避);达到上限告警。

10. 一个可落地的跨标签页消息总线(含降级)

目标:优先 BroadcastChannel → 失败则 Service Worker → 再失败则 localStorage。

// 简化版:事件订阅、可靠发送(可自行扩展 ACK/重试)
​
type Handler = (msg: any) => void;
​
export class CrossTabBus {
  private channelName: string;
  private bc?: BroadcastChannel;
  private swReady: Promise<ServiceWorkerRegistration> | null = null;
  private lsKey: string;
  private handlers: Map<string, Set<Handler>> = new Map();
  private instanceId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
  private seen = new Set<string>();
​
  constructor(channelName = 'app_bus') {
    this.channelName = channelName;
    this.lsKey = `${channelName}__ls`;
​
    // 1) BroadcastChannel
    try {
      this.bc = new BroadcastChannel(channelName);
      this.bc.onmessage = (e) => this._onMessage(e.data);
    } catch {}
​
    // 2) Service Worker(可选)
    if ('serviceWorker' in navigator) {
      this.swReady = navigator.serviceWorker.ready.catch(() => null as any);
      navigator.serviceWorker.addEventListener('message', (e) => this._onMessage(e.data));
    }
​
    // 3) localStorage 兜底
    window.addEventListener('storage', (e) => {
      if (e.key === this.lsKey && e.newValue) {
        try { this._onMessage(JSON.parse(e.newValue)); } catch {}
      }
    });
  }
​
  on(type: string, fn: Handler) {
    if (!this.handlers.has(type)) this.handlers.set(type, new Set());
    this.handlers.get(type)!.add(fn);
    return () => this.off(type, fn);
  }
​
  off(type: string, fn: Handler) {
    this.handlers.get(type)?.delete(fn);
  }
​
  emit(type: string, payload: any) {
    const msg = { id: crypto.randomUUID?.() || String(Math.random()), ts: Date.now(), type, payload, from: this.instanceId };
    this._fanout(msg);
  }
​
  private _fanout(msg: any) {
    // BroadcastChannel
    if (this.bc) {
      try { this.bc.postMessage(msg); } catch {}
    }
​
    // Service Worker(向 active SW 发送)
    this.swReady?.then((reg) => reg?.active?.postMessage?.(msg)).catch(() => {});
​
    // localStorage 兜底
    try { localStorage.setItem(this.lsKey, JSON.stringify(msg)); } catch {}
  }
​
  private _onMessage(msg: any) {
    if (!msg || typeof msg !== 'object') return;
    if (this.seen.has(msg.id)) return; // 去重
    this.seen.add(msg.id);
    // LRU 简化:超过一定大小清理
    if (this.seen.size > 2000) this.seen.clear();
​
    const set = this.handlers.get(msg.type);
    set?.forEach((fn) => fn(msg.payload));
  }
}

使用:

import { CrossTabBus } from './CrossTabBus';
​
const bus = new CrossTabBus('my_app_bus');
​
bus.on('user:logout', () => {
  // 做登出清理
  location.reload();
});
​
// 比如收到服务端事件,广播给所有标签页
function onServerLogout() {
  bus.emit('user:logout', { reason: 'token_expired' });
}

11. 选主(Leader Election)与单例任务

目的:同一站点只让一个标签页跑“定时同步/心跳/后台任务”。

11.1 BroadcastChannel + 心跳

const bc = new BroadcastChannel('leader');
const myId = crypto.randomUUID();
let isLeader = false;
let lastBeat = Date.now();
​
function tryElect() {
  // 若长时间未收到 leader 心跳,则自荐
  if (Date.now() - lastBeat > 3000 && !isLeader) {
    bc.postMessage({ type: 'ELECT', id: myId, t: Date.now() });
  }
}
​
bc.onmessage = (e) => {
  const m = e.data;
  if (m.type === 'BEAT') lastBeat = Date.now();
  if (m.type === 'ELECT') {
    // 简单“Bully”:ID 更大者胜出
    if (m.id > myId) isLeader = false; else isLeader = true;
  }
};
​
setInterval(() => {
  tryElect();
  if (isLeader) bc.postMessage({ type: 'BEAT', id: myId });
}, 1000);

更严谨可用 Web Locks API

navigator.locks.request('singleton-task', { mode: 'exclusive' }, async () => {
  // 只有获得锁的标签页会执行这里
  await runBackgroundJob();
});

12. 安全、性能与可靠性

12.1 安全

12.2 性能

12.3 可靠性

13. 兼容性与降级建议

首选 BroadcastChannel;如果环境不支持:

SharedWorker 在部分浏览器/版本支持较弱,尽量作为可选增强。

IE 等古老环境:只能用 storage 事件 / postMessage(在能拿到引用的前提下)。

14. 常见需求的实现清单

15. 小结

以上就是JavaScript实现浏览器不同标签页通信的原理与实践的详细内容,更多关于JavaScript浏览器不同标签页通信的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文