首发:浏览器中跨标签页通信
前言
Web 应用程序通常有时候需要打开多个标签页同时使用,在某些情况下,我们可能希望在多个标签页之间共享数据或同步状态。例如,当用户在一个标签页中进行某项操作时,我们可能希望在其他相关的标签页能够及时更新以反映这些变化。但是,这些标签页之间如何进行通信呢?在本文中,我们将探索浏览器中的不同窗口、标签页或 iframe 下的不同文档之间相互通信。
示例代码: Cross-Tab Communication
在线运行: Cross-Tab Communication
演示:
1. LocalStorage
- 写入数据
使用 localStorage.setItem(key, value) 方法将数据存储到 LocalStorage 中:
localStorage.setItem(
STORAGE_KEY,
`${new Date().toLocaleString()} from page: ${id}: ${input.value}`
);
- 监听数据变化
通过监听 LocalStorage 的 storage 事件来检测数据的变化,并进行相应的处理:
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_KEY) {
creatMessageElement(e.newValue);
}
});
e 指的是 StorageEvent,有以下几个特有的只读属性:
| 属性名 | 描述 |
|---|---|
key | 该属性代表被修改的键值。当被 clear() 方法清除之后该属性值为 null。 |
newValue | 该属性代表修改后的新值。当被 clear() 方法清理后或者该键值对被移除,newValue 的值为 null 。 |
oldValue | 该属性代表修改前的原值。在设置新键值对时由于没有原始值,该属性值为 null。 |
storageArea | 被操作的 storage 对象。 |
url | key 发生改变的对象所在文档的 URL 地址。 |
storage 事件仅在 不同标签页间 的 LocalStorage 数据变化 时才会触发,同一标签页内的LocalStorage 变化不会触发该事件,所以上面的写入数据和监听数据变化代码可以写在同一个页面中,并不会陷入死循环。
需要注意的是,LocalStorage 的大小限制通常在 5MB 左右,而且数据存储在本地,可能会受到 安全性 的影响。因此,在使用 LocalStorage 进行跨标签页通信时,应注意数据的大小和安全性问题。
2. BroadcastChannel
BroadcastChannel 用于在不同标签页之间广播消息。它使用类似于广播电台的模式,发送者将消息广播到所有订阅该频道的标签页,该频道在同源下的所有浏览器上下文共用,一个名称只对应一个频道。
- 以下是一个简单的示例代码:
// 创建一个监听“channel-name”频道的新频道
const channel = new BroadcastChannel('channel-name');
const buttonFun = () => {
// 发送消息
channel.postMessage(
`${new Date().toLocaleString()} from page: ${id}: ${input.value}`
);
};
// 监听频道 message 事件,频道收到消息时触发 message 事件
channel.addEventListener('message', (e) => {
creatMessageElement(e.data);
});
window.onbeforeunload = () => {
// 断开与频道的连接
channel.close();
};
3. SharedWorker
使用 Web Workers,可以在后台线程中运行 JavaScript。这样做的好处是可以在独立线程中执行费时的处理任务,渲染主线程不会被阻塞。在 worker 内,不能直接操作 DOM 节点,也不能使用 window 对象的默认方法和属性。然而你可以使用大量 window 对象之下的东西,包括 WebSockets,IndexedDB 以及 FireFox OS 专用的 Data Store API 等数据存储机制。
Web Workers 分为两种:
-
专用线程 Dedicated worker
一个专用 worker 仅能被生成它的脚本所使用,也就是只能在当前窗口当前页面使用。
-
共享线程 Shared worker
一个共享 worker 可以被多个脚本使用,即使这些脚本正在被不同的 window、iframe 或者 worker 访问,所以 SharedWorker 允许不同标签页之间共享一个后台线程,从而实现数据和消息的共享。
SharedWorker 使用语法:
new SharedWorker(URL, name|options);
参数
- URL worker 将执行的脚本URL,必须是同源。
- name (可选) worker 的名称标识,主要用于调试。
- options(可选)
创建实例时设定的可选属性的对象,可用的属性包括:
- type: worker 类型,可设定的值为 classic 或者 module. 若不指定,默认值为 classic。
- credentials: 一个指定要用于工作程序的凭据类型,可设定的值为* omit、*same-origin 或 *include。 *若不指定,或者 type 设定为 classic, 默认值为 omit (无需凭据)。
- name: worker 的名称标识,主要用于调试。
以下是一个简单的示例代码:
if (!!window.SharedWorker) {
const myWorker = new SharedWorker('./worker.js', 'share-worker-name');
const buttonFun = () => {
const message = `${new Date().toLocaleString()} from page: ${id}: ${
input.value
}`;
// 发送消息
myWorker.port.postMessage(message);
};
// 监听消息
myWorker.port.onmessage = (e) => {
creatMessageElement(e.data);
};
// 关闭窗口时
window.onbeforeunload = () => {
myWorker.port.postMessage('CLOSE');
};
} else {
alert('Your browser does not support SharedWorker');
}
worker.js 这个文件会被多个页面加载,但却可以共享数据,类似于 单例模式,虽然使用了 new 操作符,但最后获取到的数据却是一样的。
// 所有页面共享的数据
const listOfPort = [];
onconnect = (e) => {
// port 属性返回一个用于通信和控制共享 worker 的 MessagePort 对象
const port = e.ports[0];
// 如果当前页面不存在该 port 则加到列表中
if (!listOfPort.includes(port)) {
listOfPort.push(port);
}
// 监听返回的消息
port.onmessage = (e) => {
const data = e.data;
// 窗口关闭时删除该窗口的 port
if (data === 'CLOSE') {
const portIndex = listOfPort.findIndex((p) => p === port);
if (portIndex > -1) {
listOfPort.splice(portIndex, 1);
return;
}
}
// 过滤掉当前页面的 port,不给自己发送消息
listOfPort.filter((p) => p !== port).forEach((p) => p.postMessage(data));
};
// 如果使用 port.addEventListener('message',()=>{}) 方式监听则需要使用 start 来启动。
// port.start();
};
如果 worker 中使用 console.log() 将不会被打印在当前窗口的 console 中。 以 chrome 为例,需要在 chrome://inspect/#service-workers 中点击 inspect 打开的开发者工具才会打印 worker 中的 console,如下图所示:
4. window.open + window.opener
当我们使用 window.open 打开页面时,将返回一个被打开页面 window 的引用。被打开的页面可以通过 window.opener 获取到打开它的页面的引用,通过这种方式我们就将这些页面建立起联系。
以下是一个简单的示例代码:
// 点击按钮通过新窗口打开页面,并收集打开过的页面的window对象
openButton.addEventListener('click', (e) => {
const otherWindow = window.open(`${url}/WindowOpen/WindowOpen.html`);
listOfOtherWindows.push(otherWindow);
});
const buttonFun = () => {
const mseegae = `${new Date().toLocaleString()} from page: ${id}: ${
input.value
}`;
// 过滤掉已关闭的窗口,给未关闭的窗口发送消息
listOfOtherWindows
.filter((window) => !window.closed)
.forEach((window) => window.postMessage(mseegae, url));
// 给打开该窗口的窗口发送消息
if (!!window.opener) {
window.opener.postMessage(mseegae, url);
}
};
window.addEventListener('message', (e) => {
if (e.origin === url) {
creatMessageElement(e.data);
}
});
显然,这种强关联模式存在一个问题:如果页面不是通过在另一个页面内的 window.open 打开(例直接在地址栏输入),那这个联系就被打破了。
5. WebSocket
WebSocket 是一种在浏览器和服务器之间建立持久连接的协议,可以实现双向通信。通过WebSocket,我们可以在不同的标签页之间进行实时的数据传输和通信。
在跨标签通信方面,我们可以在每个标签页中都创建一个 WebSocket 连接,并通过 WebSocket 发送和接收消息。当一个标签页发送消息时,其他标签页可以通过监听 WebSocket 事件来接收消息,并做出相应的处理。
以下是一个简单的示例代码:
const ws = new WebSocket(WS_SERVER);
const buttonFun = () => {
const message = `${new Date().toLocaleString()} from page: ${id}: ${
input.value
}`;
ws.send(JSON.stringify({ id, message }));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.id !== id) {
creatMessageElement(data.message);
}
};
浏览器兼容性
浏览器兼容性最低版本:
| API | Chrome | Edge | Safari | Firefox | Opera | IE |
|---|---|---|---|---|---|---|
| LocalStorage | 4 | 12 | 4 | 3.5 | 11.5 | 8 |
| BroadcastChannel | 54 | 79 | 15.4 | 38 | 41 | 不支持 |
| SharedWorker | 5 | 79 | 5-6.1, 16 | 29 | 11.5 | 不支持 |
| window.open | 4 | 12 | 3.1 | 2 | 10 | 6 |
| WebSocket | 5 | 12 | 5 | 7 | 12.1 | 10 |
优缺点
| API | 跨域 | 优点 | 缺点 |
|---|---|---|---|
| LocalStorage | 不支持 | 1.支持存储和共享数据 | 1.需要数据存储在本地,可能会受到安全性的影响 2.不适用于实时通信,只能通过事件监听来传递数据 |
| BroadcastChannel | 不支持 | 1.支持实时通信,可以广播消息给所有订阅者 | 1.需要浏览器支持,不适用于老旧浏览器 |
| SharedWorker | 不支持 | 1.支持多个标签页共享同一个后台 线程,实现数据和消息的共享 2.不会占用渲染主线程 | 1.需要额外的开发工作,包括在后台线程中处理消息和同步数据 |
| window.open | 支持 | 1.适用于各种浏览器,不受兼容性限制。 | 1.需要从当前窗口打开另一个窗口 |
| WebSocket | 支持 | 1.支持实时双向通信,可以在不同标签页之间进行消息传递 | 1.需要服务器端的支持来处理 WebSocket 连接 |
总结
我们讨论了五种浏览器跨标签通信方法,选择合适的方法取决于具体需求和场景。如果仅需要实时通信使用 BroadcastChannel 通信是一个不错的选择,如果需要存储数据和共享数据,且不需要实时通信,可以使用 LocalStorage,对于需要复杂的协调工作和高性能的应用程序,Shared Web Workers 可能更适合,如果需要支持跨域,且仅在该窗口打开另一个窗口的情况下,适合使用 window.open + window.opener,如果有 WebSocket 服务端的支持,使用 WebSocket 实现实时通信和双向数据传递。
通过这些方法,我们可以在不同的标签页之间进行数据传递和实时通信。根据具体的需求和场景,选择适合的方法来实现跨标签通信。