跨标签页通信

跨标签页通信就是一个标签页能够发送信息给另一个标签页(Tab)。

常见的跨标签页方案如下:

推荐:

  • WebSocket
  • window.openwindow.postMessage
  • BroadCast Channel
  • Service Worker

不推荐:

  • LocalStorage/SessionStorage window.onstorage 监听
  • cookie 定时器轮询
  • IndexedDB 定时器轮询
  • SharedWorker 定时器轮询

注意

本文并不会对每一种方案的知识点本身进行详细介绍,只会介绍如何通过该方案实现跨标签页通信。关于每种方案技术点本身相关知识,可以参阅其他对应章节。

WebSocket

推荐的跨标签页通信方案,没有任何限制,唯一的缺点就是需要通过服务器中转

WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的全双工通道,属于服务器推送技术的一种。

window.openwindow.postMessage

推荐的跨标签页通信方案,几乎没有限制,只是需要一个窗口有另一个窗口的句柄,如:window.openiframe

MDN 上是这样介绍 window.postMessage 的:

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 HTTPS),端口号(443 为 HTTPS 的默认值),以及主机(两个页面的模数 Document.domain 设置为相同的值)时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数将通过消息事件对象暴露给接收消息的窗口。

// 新窗口由本窗口打开,两个窗口就有联系
// 或者是 iframe 窗口也一样
const opener = window.open("index2.html", "index2", "height=400,width=400,top=10,resizable=yes");
 
// data 代表的是发送是数据,origin 用来限制访问来源,也可以用 * 代替
opener.postMessage({ value: 123 }, "*");
window.addEventListener('message', e => {
	console.log(e.data);
}, false); // 事件监听

BroadCast Channel

推荐的跨标签页通信方案,需要是同源页面

BroadCast Channel 可以创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。但是前提是同源页面。

// 页面 1 创建一个名为 FM892 的广播频道
const chan = new BroadcastChannel('FM892');
 
// 发送消息
chan.postMessage({
	value: '123'
})
// 页面 2 创建相同名字的广播频道
const chan = new BroadcastChannel('FM892');
 
// 监听消息
chan.onmessage = function (e) {
	console.log(e.data);
};

Service Worker

推荐的跨标签页通信方案,需要是同源页面

Service Worker 实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。

Service Worker 的目的在于离线缓存,转发请求和网络代理。

// 页面 1 注册好 sw.js Service Worker
navigator.serviceWorker.register('sw.js').then(() => {
	console.log("service worker 注册成功");
});
 
// 发送消息
navigator.serviceWorker.controller.postMessage('hello');
// 页面 2 注册好相同的 Service Worker
navigator.serviceWorker.register('sw.js').then(() => {
	console.log("service worker 注册成功");
});
 
// 监听消息
navigator.serviceWorker.onmessage = function (e) {
	console.log(e.data);
}

sw.js

// 页面 1 的 navigator.serviceWorker.controller.postMessage 发的消息会发到这里
self.addEventListener("message", async event => {
	// 获取所有注册了该 Service Worker 的客户端
    const clients = await self.clients.matchAll();
    clients.forEach(client => {
	    // 将消息转发给客户端
        client.postMessage(event)
    });
});

LocalStorage/SessionStorage window.onstorage 监听

不推荐的跨标签页通信方案,需要是同源页面,副作用是要修改 Web Storage 数据

在 Web Storage 中,每次修改本地存储时,就会触发一个 storage 事件。

由事件监听器发送给回调函数的事件对象有几个自动填充的属性如下:

  • key:被修改条目的键。
  • newValue:被修改后的新值。
  • oldValue:修改前的值。
  • storageArea:指向事件监听对应的 Storage 对象。
  • url:原始触发 storage 事件的那个网页的地址。

注意

这个事件只在同一域下的任何窗口或者标签上触发,并且只在被存储的条目改变时触发(增删条目或修改值就会触发,新值和老值一样则不触发)。

本地文件 file:// 无法触发 storage 事件,要使用 HTTP/HTTPS 协议

// 监听
window.addEventListener('storage', event => {
	console.log(event)
})
 
// 或
window.onstorage = event => {
	console.log(event)
}

cookie 定时器轮询

不推荐的跨标签页通信方案,需要是同源页面,副作用是要修改 cookie 数据,且需要轮询

我们同样可以通过定时器轮询的方式来监听 Cookie 的变化,从而达到一个多标签页通信的目的。

// 页面 1 修改 cookie
document.cookie = "name=zhangsan";
// 页面 2 轮询 cookie 变化
let cookie = document.cookie;
setInterval(() => {
	if (cookie !== document.cookie) {
		cookie = document.cookie;
		console.log(`cookie 信息已经改变,最新的 cookie 值为${cookie}`);
	}
}, 1000);

IndexedDB 定时器轮询

不推荐的跨标签页通信方案,需要是同源页面,副作用是要修改 IndexedDB 数据,且需要轮询

IndexedDB 是一种底层 API,用于在客户端存储数据。该 API 使用索引实现对数据的高性能搜索。

通过对 IndexedDB 进行定时器轮询的方式,我们也能够实现跨标签页的通信。

就是一个页面定时器轮询 IndexedDB 数据是否有变化,另一个页面将需要传递的数据存到 IndexedDB 中。

SharedWorker 定时器轮询

不推荐的跨标签页通信方案,需要是同源页面,且需要轮询

SharedWorker 是 Web Worker 的一种,多个页面共享一个 SharedWorker

下面是 MDN 关于 SharedWorker 的说明:

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域,如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。

// 页面 1 使用 worker.js SharedWorker
const worker = new SharedWorker('worker.js');
 
// 发消息
worker.port.postMessage('111');
// 页面 2 使用相同的 SharedWorker
const worker = new SharedWorker('worker.js');
 
worker.port.start();
worker.port.addEventListener('message', e => {
	if (e.data) {
		console.log('来自worker的数据:', e.data)
	}
}, false);
 
setInterval(() => {
	// 获取和发送消息都是调用 postMessage 方法,我这里约定的是传递 'get' 表示获取数据。
	worker.port.postMessage('get')
}, 1000);

worker.js

let data = '';
self.onconnect = function (e) {
    const port = e.ports[0]
    port.onmessage = function (e) {
        // 如果是 get 则返回数据给客户端
        if (e.data === 'get') {       
            port.postMessage(data);
            data = "";
        } else {    
            // 否则把数据保存                  
            data = e.data
        }
    }
}