跨tab通信需求常见于一些有大量内容需要浏览或操作(比如博客或者CMS系统),同时需要保持数据一致性的场景。这些场景大部分可以引入后端服务,通过轮询等方式保持多个tab间的数据一致。本文介绍的是在不引入后端服务的情况下,通过纯前端的方案,来进行跨tab通信

什么场景下需要跨tab通信?

有一些日常中很常见的场景,如果支持跨tab通信,可能能给用户更好的体验。

博客Dark Mode切换

想象你在浏览 Dan Abramov的博客,你同时打开了好几个tab学习一系列知识。夜晚来临,你将一个页面切换成了Dark Mode。当你阅读完这一篇去阅读下一篇时,页面却不是以Dark Mode呈现的。你不得不按下F5刷新页面来寻求一致的阅读体验。

需要频繁登录的CMS系统

想象你在网店管理系统中管理你网店中的商品,你打开了好几个tab,分别管理商品类型,商品详情以及商品库存。你正在管理你的商品详情,突然你的登录session过期,你点击重新登录。但是当你切换到商品库存页面时,你发现你依旧无法操作,需要再次登录。你不得已点击了登录按钮,和你辛苦编辑了10分钟但没有保存的商品库存说了拜拜。

以上场景其实无伤大雅,你的系统依旧运转,核心功能依旧正常。但是借助跨tab通信,你能给用户提供更好更顺滑的体验。

要实现数据的一致,很容易想到的就是统一的数据存储。在有后端加入的时候,后端其实就是前端的统一数据存储。在纯前端的环境下,比较常见的数据存储包括

sessionStorage 由于只存活于单个tab session中,所以不能用于跨tab通信。localStorage 有其他的应用方式,下面会深入分析。所以在这个章节我们主要关注cookie和IndexedDB。

实现思路

实现思路其实非常简单,就是通过轮询数据源,来确保每个tab的数据都是最新的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Cookie Way
// Producer Tab, 当然你也可以用js-cookie这种库更高效的操作cookie
document.cookie = "blabla"

// Consumer Tab
setInterval(() => {
  const cookie = document.cookie;
  //Use Cookie to do something
  doSomething(cookie)
}, 1000)

// IndexedDB way
// Producer Tab
const transaction = db.transaction(["customers"], "readwrite");

const objectStore = transaction.objectStore("customers");
customerData.forEach(function(customer) {
  const request = objectStore.add(customer);
  request.onsuccess = function(event) {
    // do some callback
  };
});

// Consumer Tab
setInterval(() => {
  const transaction = db.transaction(["customers"]);
  const objectStore = transaction.objectStore("customers");
  const request = objectStore.get("444-44-4444");
  request.onsuccess = function(event) {
    doSomething(request.result.name);
};
}, 1000)

需要注意的是,cookie的读写是同步的,在cookie比较大的时候可能会阻塞主线程造成页面卡顿。而IndexedDB的API都是异步的,所以不会有cookie同步读写的性能问题。

采用这种统一数据源的方式实现跨tab通信思路上比较简单,但是轮询的方式在数据变更不频繁的情况下效率是比较低下的(本质上是一种consumer pull的方式)。所以更理想的情况是,通过事件去驱动consumer(producer push的方式)做出对应的响应。

localStorage Event

localStorage本质上也是一种数据源,但是不同于上面提到的两种数据源,当localStorage存储内容发生变更时,会发出对应的事件,同源下的所有tab都能接收到该事件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Producer Tab
window.localStorage.setItem("foo", "bar");

// Consumer Tab
window.addEventListener('storage', (event) => {
 if (event.storageArea != localStorage) return;
 if (event.key === 'foo') {
   // Do something with event.newValue
 }
});

当然这种方式也有一些弊端,使用时需要注意

  • 和cookie类似,localStorage的读写也是同步的,所以当storage数据量很大的时候,读写可能本身就会阻塞主线程造成页面卡顿。
  • localStorage 的value 只支持string,在传递复杂数据时稍显麻烦。
  • 触发 setItem 的页面不会收到 storage event (FYI: https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent)。

Broadcast Channel

Broadcast Channel 是一个Web API,能够实现同源下跨tab,windows,frames,iframes甚至是workers的通信,同时不像localStorage,发送的消息对象可以是任何Object类型,非常适合用于跨tab通信。

1
2
3
4
5
6
7
8
// Producer Tab
// init channel
const bc = new BroadcastChannel('test_channel');
// send message
bc.postMessage('This is a test message.');

// Consumer Tab
bc.onmessage = function (ev) { doSomething(ev); }

Broadcast Channel 的弊端在于其浏览器兼容性,直至本文发布,Safari仍不支持 Broadcast Channel (FYI https://caniuse.com/?search=broadcast)。

ServiceWorker

ServiceWorker 更常见的场景可能是缓存数据和加载数据,提升网页加载速度。但是ServiceWorker本身也是支持发送消息的,因此也可以用于跨tab通信。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Producer Tab
navigator.serviceWorker.controller.postMessage({
 broadcast: data
});

// Consumer Tab
addEventListener('message', async (event) => {
 if ('boadcast' in event.data ) {
  const allClients = await clients.matchAll();
  for (const client of allClients) {
   client.postMessage(event.broadcast);
  }
 }
});

使用 ServiceWorker 做跨tab通信的弊端在于,你需要学习和了解ServiceWorker,如果你的网站本来没有使用ServiceWorker,仅仅为了跨tab通信就去使用ServiceWorker,或许会有一点本末倒置。

window.postMessage

上面提到的几种方式,都只能在同源情况下进行跨tab通信,如果要不同源的tab也能通信,就只能借助 window.postMessage 了。

1
2
3
4
5
6
7
8
9
// Producer Tab
targetWindow.postMessage(message, targetOrigin)

// Consumer Tab
window.addEventListener("message", (event) => {
  if (event.origin !== targetOrigin)
    return;
  // Do something
}, false);

总结

  • 使用cookie或者IndexedDB可以简单的实现跨tab通信,但是需要考虑轮询的性能问题
  • localStorage Event可以跨tab传输,但是也需要考虑性能以及一些边界场景
  • 如果没有浏览器兼容性要求,Broadcast Channel或许是最好的选择
  • ServiceWorker也可进行跨tab通信,但是你需要提前评估引入ServiceWorker的成本
  • 如果需要跨域进行tab间通信,只能选择 window.postMessage

参考资料