浏览器标签页间通信

实现同源标签页之间数据通信的多种方法

问题

如何实现浏览器内多个标签页之间的通信?

解答

Broadcast Channel

用于同源不同页面之间完成通信,在某个页面发送的消息会被其他页面监听到。注意该方法无法完成跨域的数据传输。

// 页面 A
const channel = new BroadcastChannel('my_channel');
channel.postMessage('Hello from A');

// 页面 B
const channel = new BroadcastChannel('my_channel');
channel.onmessage = (e) => {
  console.log(e.data); // 'Hello from A'
};

localStorage

localStorage 是浏览器多个标签共用的存储空间,可以用来实现多标签之间的通信(session 是会话级的存储空间,每个标签页都是单独的)。

// 页面 A
localStorage.setItem('message', 'Hello');

// 页面 B
window.addEventListener('storage', (e) => {
  if (e.key === 'message') {
    console.log(e.newValue); // 'Hello'
  }
});

SharedWorker

SharedWorker 可以被多个 window 共同使用,但必须保证这些标签页都是同源的(相同的协议、主机和端口号)。

// 创建 SharedWorker
const worker = new SharedWorker('worker.js');

// 页面 A
worker.port.postMessage('Hello from A');

// 页面 B
worker.port.onmessage = (e) => {
  console.log(e.data);
};

WebSocket

全双工通信可以实现多个标签之间的通信,需要服务器支持。

const socket = new WebSocket('ws://localhost:8080');

socket.onmessage = (event) => {
  console.log(event.data);
};

socket.send('Hello');

postMessage

适用于两个有依赖关系的标签页,如 A 页面通过 window.open 打开 B 页面,或 B 页面通过 iframe 嵌入至 A 页面。

// A 页面打开 B 页面
const windowB = window.open('b.html');
windowB.postMessage('Hello from A', '/');

// B 页面
window.addEventListener('message', (e) => {
  let { data, source, origin } = e;
  console.log(data); // 'Hello from A'
  // B 页面回复 A 页面
  source.postMessage('message echo', '/');
});

postMessage 的第一个参数为消息实体,它是一个结构化对象;第二个参数为消息发送范围选择器,设置为 '/' 意味着只发送消息给同源的页面,设置为 '*' 则发送全部页面。

通过定时器不断检查 cookie 的值是否发生变化。由于 cookie 在同域可读,页面 B 改变 cookie 的值,页面 A 可以检测到。

// 页面 A
setInterval(() => {
  const message = document.cookie.match(/message=([^;]+)/)?.[1];
  if (message) {
    console.log(message);
  }
}, 1000);

// 页面 B
document.cookie = 'message=Hello';

这种方法相当浪费资源,不够优雅,不推荐使用。

关键点

  • Broadcast Channel 和 localStorage 是最简单的同源通信方式
  • SharedWorker 适合需要共享计算逻辑的场景
  • postMessage 适用于有父子或 iframe 关系的页面
  • WebSocket 需要服务器支持,但可以实现实时双向通信
  • 避免使用 setInterval + cookie 的方式,性能差且不优雅