字节跳动:前端专家6种Cross-Tab双工通信技术

前有科技后进阶 2024-03-01 18:13:25

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

多年来,由于 Web 应用程序的需求,Web 浏览器的功能不断增强。 因此,可以找到多种方法来实现类似的功能,比如:跨浏览器选项卡进行通信的能力。

该能力的典型场景包括:

更改应用程序的主题(例如,深色或浅色主题)会在已打开的浏览器选项卡中生效检索最新的身份验证令牌并在浏览器选项卡之间共享。跨浏览器选项卡同步应用程序状态。

接下来一起聊聊不同方案的特点。

1.LocalStorage 事件

LocalStorage 可以支持同源应用跨选项卡访问,同时也支持事件,可以使用此功能跨浏览器选项卡进行通信,一旦存储更新,其他选项卡将收到storage事件。

例如在一个选项卡中执行以下 JavaScript 代码:

window.localStorage.setItem('loggedIn', 'true');

其他选项卡也能够收到相应的storage事件:

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

然而,该方法有一些限制,比如:

必须同源,网站 A 不能访问网站 B 的 LocalStorage执行存储设置操作的选项卡,不会触发此事件。大量数据场景下该方法会产生不利影响,因为 LocalStorage 是同步的,因此可能阻塞主 UI 线程。2.BroadcastChannel2.1 BroadcastChannel 用法

BroadcastChannel API 允许选项卡、窗口、框架、Iframe 和 Web Workers 之间进行通信,一个选项卡可以创建并发布。

const channel = new BroadcastChannel('app-data');channel.postMessage(data);

其他 Tab 可以按照下面的方法来监听:

const channel = new BroadcastChannel('app-data');channel.addEventListener('message', (event) => { console.log(event.data);});

这样,浏览器上下文,如:Windows、选项卡、框架或 Iframe 等就可以进行通信,这是目前为止浏览器选项卡之间通信的一种最便捷的方式。

2.2 BroadcastChannel 优缺点

BroadcastChannel 优点包括:

可靠传递数据:提供了一种可靠的方法,使独立的 JavaScript 应用程序在同一浏览器同一站点内传递数据。传输速度快:以高速连接,提供更快的数据传输速度。实时性:提供了实时,低延迟的数据传输。可靠性:能够在小的数据包丢失或意外丢失时进行恢复。

不过,BroadcastChannel API 也存在以下缺点:

仅限同源:BroadcastChannel API 只能在同一浏览器同一站点内进行通信。受浏览器支持限制:与大多数 Web API 一样,BroadcastChannel API 受到不同浏览器和平台的支持和兼容性影响,如 safari 和 IE 并不支持这种方式。2.3 BroadcastChannel 与其他方案差异MessageChannel vs. BroadcastChannel

MessageChannel 和 后面介绍的 BroadcastChannel 之间的主要区别在于,后者是将消息分派给多个侦听器(一对多)的方法,而 MessageChannel 用于脚本之间直接进行一对一通信。

BroadcastChannel vs. postMessage

与 postMessage() 不同,开发者不再需要维护对 iframe 或工作线程的引用才能与其通信:

// 不需要保存window对象的引用const popup = window.open('https://another-origin.com', ...);popup.postMessage('Sup popup!', 'https://another-origin.com');

window.postMessage() 还允许跨源通信,而 BroadcastChannel API 是同源的。由于消息保证来自同一来源,因此不需要像以前使用 window.postMessage() 那样多次验证:

// 不需验证消息来源const iframe = document.querySelector('iframe');iframe.contentWindow.onmessage = function (e) { if (e.origin !== 'https://expected-origin.com') { return; } e.source.postMessage('Ack!', e.origin);};BroadcastChannel vs. SharedWorkers

对于需要向可能多个窗口/选项卡或 worker 发送消息的简单情况,可以使用 BroadcastChannel。

对于管理锁、共享状态、服务器和多个客户端之间的资源同步或与远程主机共享 WebSocket 连接等更高级的用例,SharedWorkers 可能是更合适的解决方案。

3.Service Workers

Service Workers 支持发送消息,可以用它来跨浏览器选项卡进行通信。 在通信之前,需要使用 Service Worker 创建通信通道。下面示例代码使用 Service Worker 创建一个文件并将其注册到应用程序的代码中:

if ('serviceWorker' in navigator) { navigator.serviceWorker .register('stateSW.js') .then(function (reg) { // 注册成功 if (reg.installing) { console.log('Service worker installing'); } else if (reg.waiting) { console.log('Service worker installed'); } else if (reg.active) { console.log('Service worker active'); } }) .catch(function (error) { // registration failed console.log('Registration failed with ' + error); });}

然后,需要在 worker 的代码中订阅 postMessage 事件,并将数据传输到所有选项卡中,如下所示:

self.addEventListener('message', async (event) => { await broadcastToAllClients(event.data);});async function broadcastToAllClients(data) { const clients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window', }); clients.forEach((client) => { client.postMessage(data); });}

接下来,需要从组件本身的 Service Worker 订阅 postMessage 事件:

const listenState = (event) => { setBgColor(event.data.color);};const unsubscribe = () => { navigator.serviceWorker.removeEventListener('message', listenState);};navigator.serviceWorker.addEventListener('message', listenState);

作为最后一步,需要一个函数来向 serviceWorker 发送消息:

const dispatchState = () => { const color = pickColor(); setBgColor(color); // 向serviceWorker发送消息 navigator.serviceWorker.ready.then((registration) => { registration.active.postMessage({ color, }); });};

完整实例代码可以在这里查看,https://github.com/EugenTepin/vanilla-state-share/blob/service-worker/index.html

需要注意的是,Service Worker 本质是一个 Web Worker,独立于 JavaScript 主线程,因此不能直接访问 DOM,也不能直接访问 window 对象。但是,Service Worker 可以访问 navigator 对象,也可以通过消息传递的方式(postMessage)与 JavaScript 主线程进行通信。

不过值得注意的是,Service Workers 受到同源限制,分配给 Worker 线程运行的脚本文件必须与主线程的脚本文件同源,通常都应该放在同一项目下。

4.Window PostMessage

跨浏览器选项卡、弹出窗口和 Iframe 进行通信的传统方法之一是 Window.postMessage() 方法,可以按如下方式发送消息。

targetWindow.postMessage(message, targetOrigin);

目标窗口可以监听事件:

window.addEventListener( 'message', (event) => { if (event.origin !== 'http://localhost:8080') return; // Do something }, false);

与其他方法相比,此方法的一个优点是可以支持跨域通信, 但限制之一是开发者需要持有其他浏览器选项卡的引用。 因此,这种方法仅适用于通过 window.open() 或 document.open() 打开的浏览器选项卡。

5.SharedWorker

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如:窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域。

在下面基本的 Share Worker 示例中有两个 HTML 页面,每个页面中使用一些 JavaScript 来执行简单的计算。这些脚本使用相同的 worker 文件来执行计算,每个脚本都可以访问这个 worker 文件,即使脚本所处的页面在不同的窗口下。

下面的代码展示了如何通过 SharedWorker() 方法来创建一个共享进程对象。

var myWorker = new SharedWorker('worker.js');

然后两个脚本都通过 MessagePort 对象来访问 worker,这个对象用 SharedWorker.port 属性获得。如果已经用 addEventListener 监听了 onmessage 事件,则可以使用 start() 方法手动启动端口:

myWorker.port.start();

当启动端口时,两个脚本都会向 worker 发送消息,然后使用 port.postMessage()和 port.onmessage 处理从 worker 返回的消息:

first.onchange = function () { myWorker.port.postMessage([first.value, second.value]); console.log('Message posted to worker');};second.onchange = function () { myWorker.port.postMessage([first.value, second.value]); console.log('Message posted to worker');};myWorker.port.onmessage = function (e) { result1.textContent = e.data; console.log('Message received from worker');};

在 worker 中使用 SharedWorkerGlobalScope.onconnect 处理程序连接到上面讨论的相同端口。可以在 connect 事件的 ports 属性中获取到与该 worker 相关联的端口,然后使用 MessagePort start() 方法来启动端口,然后 onmessage 处理程序处理来自主线程的消息。

onconnect = function (e) { var port = e.ports[0]; port.addEventListener('message', function (e) { var workerResult = 'Result: ' + e.data[0] * e.data[1]; port.postMessage(workerResult); }); port.start(); // addEventListener时候需要手动执行};

需要注意的是,SharedWorker 和其他 Worker 相同,都需要同源,其是通过 url 来区分是否是同一个 SharedWorker。

6.MessageChannel

Channel Messaging API 的 MessageChannel 接口允许创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。

以下代码块使用 MessageChannel 构造函数实例化了一个 channel 对象。当 iframe 加载完毕,使用 MessagePort.postMessage 方法把一条消息和 MessageChannel.port2 传递给 iframe。

handleMessage 处理程序将会从 iframe 中(使用 MessagePort.onmessage 监听事件)接收到信息,将数据其放入 innerHTML 中。

var channel = new MessageChannel();var para = document.querySelector('p');var ifr = document.querySelector('iframe');var otherWindow = ifr.contentWindow;ifr.addEventListener('load', iframeLoaded, false);function iframeLoaded() { otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);}channel.port1.onmessage = handleMessage;function handleMessage(e) { para.innerHTML = e.data;}

在 iframe 中可以使用传递的 port 将消息传递会主页面。

const output = document.querySelector('.output');window.addEventListener('message', onMessage);function onMessage(e) { output.innerHTML = e.data; // Use the transfered port to post a message back to the main frame e.ports[0].postMessage('Message back from the IFrame');}

总之,MessageChannel 为开发者提供了一种在 Web 上的不同源(例如:跨源 iframe)之间异步传递信息的方法。 传统上,大多数开发者都会使用 window.postMessage(),但 MessageChannel 的优点是更清晰,只需要在设置时验证来源,并更好地处理委托,同时在很多场景下性能更高。

参考资料

https://blog.bitsrc.io/4-ways-to-communicate-across-browser-tabs-in-realtime-e4f5f6cbedca

https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register

https://intspirit.medium.com/comparison-of-data-transfer-methods-between-browser-tabs-in-spa-context-684c3d95c3a1

https://developer.mozilla.org/zh-CN/docs/Web/API/SharedWorker

https://googlechrome.github.io/samples/service-worker/post-message/

https://intspirit.medium.com/comparison-of-data-transfer-methods-between-browser-tabs-in-spa-context-684c3d95c3a1

https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel

https://developer.chrome.com/blog/broadcastchannel/#difference-with-sharedworkers

https://www.jefftk.com/p/overhead-of-messagechannel

https://kishoreconnect.com/javascript-cross-tab-cross-site-communication

0 阅读:123

前有科技后进阶

简介:感谢大家的关注