# 标签页通信(同源)
# 场景
- A 页面点击按钮
window.open打开同源的 B 页面,双方需要互发消息。
# 方案 1:window.postMessage + opener(推荐)
适用于已知父子关系的同源标签页。注意打开窗口时不要加 noopener / noreferrer,否则 window.opener 为 null。
<!-- 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 8080 或 python -m http.server 8080),避免 file:// 下的跨源限制。