亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JavaScript實(shí)現(xiàn)瀏覽器不同標(biāo)簽頁(yè)通信的原理與實(shí)踐

 更新時(shí)間:2025年08月29日 09:44:49   作者:Luckily_BAI  
這篇文章主要為大家詳細(xì)系統(tǒng)梳理了不同頁(yè)簽(Tab / Window / Frame)之間的通信手段以及具體的實(shí)現(xiàn)方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

系統(tǒng)梳理不同頁(yè)簽(Tab / Window / Frame)之間的通信手段:能做什么、怎么做、底層原理、限制與踩坑、降級(jí)方案,以及一份可落地的通用消息總線實(shí)現(xiàn)。

1. 典型場(chǎng)景與選擇指南

訴求推薦優(yōu)先級(jí)說明
同源多標(biāo)簽頁(yè)簡(jiǎn)單廣播BroadcastChannel → localStorage(storage 事件)現(xiàn)代瀏覽器首選 BroadcastChannel;需要兼容老瀏覽器時(shí)降級(jí)到 storage 事件
父子窗口/打開者-被打開者直接通信postMessage + Window 引用通過 window.open、window.opener、iframe.contentWindow 獲取引用,再 postMessage
需要單點(diǎn)中樞、復(fù)雜路由或持久化SharedWorker / Service WorkerSharedWorker 做內(nèi)存中樞;Service Worker 能向所有受控客戶端群發(fā),并可持久化
跨設(shè)備或登錄用戶維度的廣播WebSocket / SSE / WebRTC通過后端轉(zhuǎn)發(fā)(或點(diǎn)對(duì)點(diǎn))實(shí)現(xiàn)端到端同步
單例任務(wù)/互斥鎖(而非消息)Web Locks API(navigator.locks)不是通信,但常與通信結(jié)合做“選主/互斥”

原則:能用 BroadcastChannel 就別用 localStorage(更干凈、性能好、不阻塞)。要兼容老環(huán)境,再做降級(jí);有復(fù)雜編排再上 Worker。

2. BroadcastChannel:同源廣播的首選

2.1 核心 API

// 建立頻道
const bc = new BroadcastChannel('app_bus');
?
// 接收
bc.onmessage = (event) => {
  // event.data 支持 Structured Clone(可傳對(duì)象、ArrayBuffer、MessagePort 等)
  console.log('[BC] recv:', event.data);
};
?
// 發(fā)送
bc.postMessage({ type: 'PING', ts: Date.now() });
?
// 關(guān)閉
bc.close();

2.2 原理與語(yǔ)義

  • 同源標(biāo)簽頁(yè)共享同名頻道;瀏覽器內(nèi)部維護(hù)訂閱者列表,消息采用 Structured Clone 語(yǔ)義拷貝。
  • 有序性:同一發(fā)送方的消息在同一接收方表現(xiàn)為 FIFO,但不同發(fā)送方之間沒有全局序。
  • 傳輸保障:非持久、非可靠(頁(yè)面關(guān)閉或后臺(tái)凍結(jié)時(shí)可能丟)。

2.3 實(shí)戰(zhàn)要點(diǎn)

  • 大消息(> 數(shù)百 KB)不建議直傳,改為 “信令 + IndexedDB 取件”
  • 頁(yè)面卸載時(shí)(visibilitychange/pagehide)清理資源;必要時(shí)發(fā)送 LEAVE
  • 去重:消息附帶 id(如 crypto.randomUUID()),接收方維護(hù) LRU Set 去重。

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

3.1 使用示例

// 監(jiān)聽(只在“其他”標(biāo)簽頁(yè)觸發(fā))
window.addEventListener('storage', (e) => {
  if (e.key === 'app_bus') {
    const payload = JSON.parse(e.newValue || 'null');
    if (payload) handleMessage(payload);
  }
});
?
// 發(fā)送(會(huì)同步寫磁盤;同頁(yè)不觸發(fā) storage 事件)
function send(msg) {
  localStorage.setItem('app_bus', JSON.stringify({
    ...msg,
    id: crypto.randomUUID(),
    ts: Date.now(),
  }));
}

3.2 限制與坑

  • 同步阻塞:setItem 會(huì)阻塞主線程,頻繁發(fā)送會(huì)卡頓。
  • 同頁(yè)不觸發(fā):當(dāng)前頁(yè)調(diào)用 setItem 不會(huì)觸發(fā)本頁(yè) storage 事件。
  • 只有字符串:需要 JSON 編解碼。
  • 同值不觸發(fā):如果寫入的值未變化,不會(huì)觸發(fā)事件(可附帶隨機(jī) nonce)。
  • 隱私模式/分區(qū)存儲(chǔ):不同容器/場(chǎng)景可能彼此隔離。

適用于“偶發(fā)廣播 + 低頻消息 + 老瀏覽器兼容”。

4. postMessage + Window 引用:點(diǎn)對(duì)點(diǎn)直連

4.1 適用場(chǎng)景

  • A 打開 B(window.open),或 A 內(nèi)嵌 B(iframe),或 B 由 A 打開(window.opener)。
  • 同源可直接讀寫;跨源也能 postMessage,但需 targetOrigin 校驗(yàn)。

4.2 使用示例

// 打開子窗口并握手
const child = window.open('/child.html', 'child');
?
window.addEventListener('message', (event) => {
  // 嚴(yán)格校驗(yàn)來源
  if (event.origin !== location.origin) return;
  console.log('from child:', event.data);
});
?
// 向子窗口發(fā)送
child?.postMessage({ type: 'PING' }, location.origin);

跨源時(shí):必須使用精確的 targetOrigin(不要用 *),并對(duì) event.origin 做白名單校驗(yàn),防止 XSS/點(diǎn)擊劫持類問題。

5. SharedWorker:內(nèi)存中樞

5.1 思路

多個(gè)同源頁(yè)面連接到同一個(gè) SharedWorker,通過 MessagePort 與之通信;Worker 內(nèi)部充當(dāng)“Hub”做路由/廣播/狀態(tài)管理。

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));
};

頁(yè)面:

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 優(yōu)缺點(diǎn)

  • 靈活、可做復(fù)雜編排/緩存;
  • 兼容性較 BroadcastChannel 差一些;調(diào)試門檻略高。

6. Service Worker:全客戶端中轉(zhuǎn)與持久化

6.1 模式

  • 頁(yè)面 → SW:navigator.serviceWorker.controller.postMessage
  • SW → 所有頁(yè)面:self.clients.matchAll() → client.postMessage

6.2 示例

sw.js

self.addEventListener('message', (event) => {
  // 簡(jiǎn)單群發(fā)
  self.clients.matchAll({ includeUncontrolled: true, type: 'window' })
    .then((clients) => {
      clients.forEach((client) => client.postMessage(event.data));
    });
});
?
self.addEventListener('activate', (e) => self.clients.claim());

頁(yè)面:

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 要點(diǎn)

  • 僅受控頁(yè)面能直接與 SW 通信;首次加載可能未受控,clients.claim() 可加速接管。
  • SW 可結(jié)合 Cache/IndexedDB 做可靠隊(duì)列或離線重放。

7. 服務(wù)器中轉(zhuǎn)(WebSocket / SSE / WebRTC)

  • 跨設(shè)備/賬號(hào)級(jí)廣播的標(biāo)準(zhǔn)方案;同設(shè)備多標(biāo)簽頁(yè)也可統(tǒng)一走服務(wù)器,減少本地復(fù)雜度。
  • WebSocket:雙向、低時(shí)延;SSE:?jiǎn)蜗?、?jiǎn)單;WebRTC:P2P,常與信令(WebSocket)結(jié)合。

實(shí)戰(zhàn)建議:本地(BC/Worker)優(yōu)先、服務(wù)器兜底。對(duì)“必須送達(dá)”的關(guān)鍵消息,做ACK + 重試。

8. 更高階:SharedArrayBuffer(SAB)與跨上下文共享

跨源隔離(COOP+COEP)啟用時(shí),可通過 BroadcastChannel / postMessage 傳遞 SharedArrayBuffer,配合 Atomics 做無鎖隊(duì)列/環(huán)形緩沖,獲得極低延遲。

復(fù)雜且對(duì)環(huán)境要求高,通常用于多媒體/計(jì)算密集型場(chǎng)景。

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

9.1 消息格式

interface BusMessage<T=any> {
  id: string;           // 唯一 ID(UUID v4)
  ts: number;           // 發(fā)送時(shí)間戳
  type: string;         // 主題/事件名
  payload: T;           // 載荷
  ack?: boolean;        // 是否為 ACK 消息
  to?: string;          // 指定接收者(可選)
  from?: string;        // 發(fā)送者實(shí)例 ID
}

9.2 接收側(cè)去重

維護(hù) seenIds(如 LRU 緩存 1–5 分鐘),收到重復(fù) id 直接丟棄。

9.3 ACK + 重試

發(fā)送后 setTimeout 等待 ACK;超時(shí)重發(fā)(指數(shù)退避);達(dá)到上限告警。

10. 一個(gè)可落地的跨標(biāo)簽頁(yè)消息總線(含降級(jí))

目標(biāo):優(yōu)先 BroadcastChannel → 失敗則 Service Worker → 再失敗則 localStorage。

// 簡(jiǎn)化版:事件訂閱、可靠發(fā)送(可自行擴(kuò)展 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 發(fā)送)
    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 簡(jiǎn)化:超過一定大小清理
    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();
});
?
// 比如收到服務(wù)端事件,廣播給所有標(biāo)簽頁(yè)
function onServerLogout() {
  bus.emit('user:logout', { reason: 'token_expired' });
}

11. 選主(Leader Election)與單例任務(wù)

目的:同一站點(diǎn)只讓一個(gè)標(biāo)簽頁(yè)跑“定時(shí)同步/心跳/后臺(tái)任務(wù)”。

11.1 BroadcastChannel + 心跳

const bc = new BroadcastChannel('leader');
const myId = crypto.randomUUID();
let isLeader = false;
let lastBeat = Date.now();
?
function tryElect() {
  // 若長(zhǎng)時(shí)間未收到 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') {
    // 簡(jiǎn)單“Bully”:ID 更大者勝出
    if (m.id > myId) isLeader = false; else isLeader = true;
  }
};
?
setInterval(() => {
  tryElect();
  if (isLeader) bc.postMessage({ type: 'BEAT', id: myId });
}, 1000);

更嚴(yán)謹(jǐn)可用 Web Locks API

navigator.locks.request('singleton-task', { mode: 'exclusive' }, async () => {
  // 只有獲得鎖的標(biāo)簽頁(yè)會(huì)執(zhí)行這里
  await runBackgroundJob();
});

12. 安全、性能與可靠性

12.1 安全

  • postMessage 必須指定精確 targetOrigin,并校驗(yàn) event.origin。
  • 對(duì)所有外部輸入(消息)做結(jié)構(gòu)校驗(yàn)(如 zod/superstruct)。
  • 不要把敏感數(shù)據(jù)放入 localStorage 明文廣播。

12.2 性能

  • localStorage.setItem 會(huì)阻塞主線程;高頻通信避免使用。
  • 大對(duì)象建議經(jīng) IndexedDB 持久化,消息只帶“索引”。
  • 背景標(biāo)簽頁(yè)計(jì)時(shí)器可能被節(jié)流,心跳/重試策略要容忍抖動(dòng)。

12.3 可靠性

  • 關(guān)鍵消息實(shí)現(xiàn) ACK/重試/去重;
  • 頁(yè)面卸載前(pagehide/visibilitychange)做最后通知;
  • 對(duì) SW/SharedWorker 引入 健康檢查重新連接。

13. 兼容性與降級(jí)建議

首選 BroadcastChannel;如果環(huán)境不支持:

  • 若存在 SW:走 SW 中轉(zhuǎn);
  • 否則:storage 事件兜底。

SharedWorker 在部分瀏覽器/版本支持較弱,盡量作為可選增強(qiáng)。

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

14. 常見需求的實(shí)現(xiàn)清單

  • 跨標(biāo)簽頁(yè)單點(diǎn)登錄/登出同步:Bus 廣播 user:logout,收到后清 Token + 刷新。
  • 表單協(xié)同編輯(同賬號(hào)) :頻道內(nèi)發(fā)送 cursor/patch,并對(duì)本地變更做去抖與去重。
  • 通知徽標(biāo)同步:收到服務(wù)器通知后在一個(gè)標(biāo)簽頁(yè)拉取計(jì)數(shù),再?gòu)V播至其他標(biāo)簽頁(yè)。
  • “只保留一個(gè)播放實(shí)例” :選主后非 Leader 收到 play 指令時(shí)轉(zhuǎn)成 pause。

15. 小結(jié)

  • BroadcastChannel 是同源多頁(yè)簽通信的“現(xiàn)代默認(rèn)”;
  • localStorage(storage 事件) 是簡(jiǎn)易兼容兜底;
  • postMessage 適用于存在窗口引用的點(diǎn)對(duì)點(diǎn)場(chǎng)景;
  • SharedWorker / Service Worker 能承載更復(fù)雜的中樞化邏輯;
  • 做到可觀測(cè)、可恢復(fù)、可降級(jí),你的跨頁(yè)通信就能穩(wěn)如老狗。

以上就是JavaScript實(shí)現(xiàn)瀏覽器不同標(biāo)簽頁(yè)通信的原理與實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于JavaScript瀏覽器不同標(biāo)簽頁(yè)通信的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論