# 标签页通信(同源)

# 场景

  • A 页面点击按钮 window.open 打开同源的 B 页面,双方需要互发消息。

# 方案 1:window.postMessage + opener(推荐)

适用于已知父子关系的同源标签页。注意打开窗口时不要加 noopener / noreferrer,否则 window.openernull

<!-- A 页面示例 -->
<button id="openB">打开 B</button>
<script>
  let childWin = null;
  document.getElementById('openB').onclick = () => {
    childWin = window.open('/b.html', '_blank');
  };

  // 监听 B 回来的消息
  window.addEventListener('message', (event) => {
    if (event.origin !== location.origin) return;
    console.log('A 收到:', event.data);
  });

  // 发消息给 B
  function sendToB(msg) {
    if (childWin && !childWin.closed) {
      childWin.postMessage({ from: 'A', msg }, location.origin);
    }
  }
</script>
<!-- B 页面示例 -->
<script>
  // 收到 A 的消息
  window.addEventListener('message', (event) => {
    if (event.origin !== location.origin) return;
    console.log('B 收到:', event.data);
    // 回一条
    if (window.opener) {
      window.opener.postMessage({ from: 'B', reply: '收到' }, event.origin);
    }
  });
</script>
  • 双方都用 message 事件,并校验 event.origin
  • 建议发消息时也传入精确的 targetOrigin(同源可用 location.origin)。

# 方案 2:BroadcastChannel(同源广播)

无需窗口引用,同源的任意标签页或 iframe 只要共用频道名即可互通。

const channel = new BroadcastChannel('demo_channel');
channel.onmessage = (event) => console.log('收到:', event.data);
channel.postMessage({ from: 'tab', msg: 'hello' });

// 离开前关闭,避免内存泄漏
window.addEventListener('beforeunload', () => channel.close());

优点:实现最简单;缺点:旧版 Safari 兼容性较差。

# 进阶示例(带 UI)

<input id="msg" value="hi from this tab" />
<button id="send">发送</button>
<pre id="log"></pre>
<script>
  const channel = new BroadcastChannel('demo_channel');
  const log = (txt) => (logEl.textContent += txt + '\n');
  const logEl = document.getElementById('log');

  channel.onmessage = (e) => log(`收到: ${JSON.stringify(e.data)}`);
  document.getElementById('send').onclick = () => {
    const text = document.getElementById('msg').value;
    channel.postMessage({ from: location.pathname, text, t: Date.now() });
    log(`发送: ${text}`);
  };

  window.addEventListener('beforeunload', () => channel.close());
</script>

# 方案 3:storage 事件

localStorage 的写操作会触发其它同源标签页的 storage 事件(当前写入页不会触发)。

// 写入方
localStorage.setItem('msg', JSON.stringify({ t: Date.now(), text: 'hi' }));

// 监听方
window.addEventListener('storage', (e) => {
  if (e.key === 'msg' && e.newValue) {
    console.log('收到 storage 消息:', JSON.parse(e.newValue));
  }
});

// 可选:清理以避免旧数据干扰
window.addEventListener('beforeunload', () => localStorage.removeItem('msg'));

# 进阶示例(防抖 + 渲染)

<input id="msg" value="hi via storage" />
<button id="send">发送</button>
<pre id="log"></pre>
<script>
  const key = 'tab_msg';
  const logEl = document.getElementById('log');
  const log = (txt) => (logEl.textContent += txt + '\n');

  document.getElementById('send').onclick = () => {
    const text = document.getElementById('msg').value;
    // 加时间戳,避免相同内容导致部分浏览器不触发变更
    localStorage.setItem(key, JSON.stringify({ text, t: Date.now(), from: location.pathname }));
    log(`发送: ${text}`);
  };

  window.addEventListener('storage', (e) => {
    if (e.key !== key || !e.newValue) return;
    const data = JSON.parse(e.newValue);
    // 简单防抖:忽略 100ms 内的重复
    const now = Date.now();
    if (window._last && now - window._last < 100) return;
    window._last = now;
    log(`收到: ${data.text} @ ${new Date(data.t).toLocaleTimeString()}`);
  });

  window.addEventListener('beforeunload', () => localStorage.removeItem(key));
</script>

注意:storage 事件只在「其它同源标签页」触发,当前写入页不会触发。

# 可运行示例

已在 docs/tab-communication/ 下放置可直接打开的示例:

  • a.html / b.html:演示方案 1(postMessage + opener)。
  • broadcast-channel.html:演示方案 2(BroadcastChannel)。
  • storage.html:演示方案 3(storage 事件)。

启动本地静态服务后访问对应路径(示例命令:npx http-server docs -p 8080python -m http.server 8080),避免 file:// 下的跨源限制。