在浏览器中,我们可以同时打开多个Tab页, 每个Tab可以粗略理解为一个“独立的运行环境”,即使是全局对象也不会在多个Tab间共享。 然而有些时候, 我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或者状态。
同源页面间的跨页面通信
1. BroadCast Channel
BroadCast Channel可以帮助我们创建一个用于广播的通信频道。 当所有页面都监听同一频道的消息时, 其中某一个页面通过它发送的消息就会被其他所有页面收到。使用方法也很简单:
创建一个标识为 bc
的频道
const bc = new BroadcastChannel('bc');
发送消息只需要调用实例上的postMessage
方法即可
bc.postMessage(data);
在需要获取数据的页面通过onmessage
来监听被广播的消息
bc.onmessage = function(e) {
console.log(e.data);
}
2. Service Worker
Service Worker是一个可以长期运行在后台的Worker, 能够实现与页面的双向通信。多页面共享间的Service Worker可以共享,将Service Worker作为消息的处理中心(中央站)即可实现广播效果。
注册Service Worker
navigator.serviceWorker.register('./sw.js').then(_ => {
console.log('Service Worker注册成功');
});
其中./sw.js
是对应的Service Worker脚本。Service Worker本身并不具备“广播通信”的功能, 需要我们将其改造成消息中转站:
self.addEventListener('message', function (e) {
console.log(e.data);
e.waitUntil(
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data);
});
})
);
});
我们在Service Worker中监听message
事件,获取页面(从Service Worker角度解释“页面”就是client)发送的消息。然后通过self.clients.matchAll()
获取当前注册了该Service Worker的所有页面, 通过调用每个client(页面)的postMessage
方法, 向页面发送消息。
发送消息,可以调用Service Worker的postMessage方法:
navigator.serviceWorker.controller.postMessage(data);
在需要获取的页面监听Service Worker发送来的消息:
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data);
});
3. LocalStorage
LocalStorage 作为前端最常用的本地存储, 但StorageEvent
这个与它相关的事件却很少用到。
当LocalStorage变化时,会触发storage
事件。 利用这个特性, 我们可以在发送消息时, 把消息写入到某个LocalStorage中;然后通过监听storage
比对key 即可获取需要的通知。
首先set一个data
window.localStorage.setItem('string', data);
监听获取data
window.addEventListener('storage', function (e) {
if (e.key === 'string') {
console.log(e.newValue);
}
});
注意: storage
事件只有在值发生变化时才会触发。
以上三种方法都是“广播模式”:一个页面将消息通知发给“中央站”,再由中央站通知各个页面。下面介绍下“共享存储+轮训模式”。
4.Shared Worker
Shared Worker是Worker家族中的另一个成员。 普通的Worker之间是独立运行, 数据不互通; 而多个Tab注册的Shared Worker则可以实现数据共享。
Shared Worker无法主动通知所有页面, 因此需使用轮训方式来拉取最新的数据。思路如下:
让Shared Worker支持两种消息。 一种是post, Shared Worker收到后会将数据保存下来; 另一种是get, Shared Worker收到消息会将保存的数据通过PostMessage传给注册它的页面。也就是说页面通过get来主动获取最新消息。
// 启动注册Shared Worker
const sharedWoker = new SharedWoker('./shared.js', 'shared');
然后再该Shared Woker中支持get 与 post形式的消息:
let data = null;
self.addEventListener('connect', function (e) {
const port = e.ports[0];
port.addEventListener('message', function (event) {
// get 指令返回存储的消息数据
if (event.data.get) {
data && port.postMessage(data);
}
// 非 get 指令存储该消息数据
else {
data = event.data;
}
});
port.start();
});
发送消息只需要调用postMessage
即可:
sharedWorker.port.postMessage(data);
页面定时发送get指令来轮训最新的消息数据,并在页面监听返回的消息:
// 定时轮询,发送 get 指令的消息
setInterval(function () {
sharedWorker.port.postMessage({get: true});
}, 1000);
// 监听 get 消息的返回数据
sharedWorker.port.addEventListener('message', (e) => {
console.log(e.data);
}, false);
sharedWorker.port.start();
注意: 如果使用
addEventLitener
来添加Shared Worker的消息监听, 需要显式调用sharedWorker.port.start()
方法;如果使用onmessage
绑定监听则不需要。
5.除了使用Shared Worker来共享数据, 还可以使用其他一些全局性的存储方案,例如 IndexDB 或者cookie。
6. window.open + window.opener
当我们使用window.open
打开页面时,方法会返回一个被打开页面window的引用。 而在未指定noopener时, 被打开的页面可以通过window.opener
获取打开它页面的引用.
window.open('/home.html');
home.html获取opener
console.log(window.opener);
非同源页面之间的通信
上面介绍的所有跨页面通信方法都受到同源策略的限制。 要实现非同源页面通信, 可以使用一个用户不可见的iframe
作为桥。 由于iframe与父页面间可以通过指定origin
来忽略同源限制, 因此可以在页面中嵌入iframe。
发送消息: “*”
可以替换为自己的url
window.frames[0].window.postMessage(data,"*");
获取消息:
window.addEventListener('message', function (e) {
console.log(e);
});
总结
对于同源页面:
- 广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
- 共享存储模式:Shared Worker / IndexedDB / cookie
- 口口相传模式:window.open + window.opener
- 基于服务端:Websocket / Comet / SSE 等
对于非同源页面:
使用 iframe作为桥发送和监听消息