浏览器跨 Tab 窗口通信原理及应用实践
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
最近,相信大家一定被这么个动效给刷屏了: 以至于,基于这个效果的二次创作层出不穷,眼花缭乱。 基于跨窗口通信的弹弹球: 基于跨窗口通信的 Flippy Bird: 我也尝试制作了一个跨 Tab 窗口的 CSS 动画联动,效果如下: 代码不多,核心代码 200 行,感兴趣的可以戳这里:Github - broadcastAnimation 当然,本文的核心不是去一一剖析上面的效果具体的实现方式,而是讲讲其中比较关键的一个技术点: 而是应用如何在多窗口下进行互相通信。 所谓多窗口下进行互相通信,是指在浏览器中,不同窗口(包括不同标签页、不同浏览器窗口甚至不同浏览器实例)之间进行数据传输和通信的能力。 当然,本文我们探讨的是纯前端的跨 Tab 页面通信,在非纯前端的方式下,我们可以借助诸如 Web Socket 等方式,藉由后端这个中间载体,进行跨页面通信。 因此,本文我们更多的重心将放在,如何基于纯前端技术,实现多窗口下进行互相通信。 为了实现跨窗口通信,它应该需要具备以下能力:
方式一:Broadcast Channel()Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,允许一个窗口发送消息,并由其他窗口接收。 其核心步骤如下:
同时,Broadcast Channel 遵循浏览器的同源策略。这意味着只有在同一个协议、主机和端口下的窗口才能正常进行通信。如果窗口不满足同源策略,将无法互相发送和接收消息。 因为有同源限制,我们需要起一个服务,这里我基于 Vite 快速起了一个 Vue 项目,简单的基于 其核心代码非常简单: <template> <div id="j-main"> // ... </div> </template> <script> import { onMounted } from 'vue'; export default { setup() { function createBroadcastChannel() { broadcastChannel = new BroadcastChannel('broadcast'); broadcastChannel.onmessage = handleMessage; } function sendMessage(data) { broadcastChannel.postMessage(data); } function handleMessage(event) { console.log('接收到 event', event); // TODO: 处理接收到信息后的逻辑 } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); sendMessage(pos); }); } // 计算当前元素距离显示器窗口右上角的距离 function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 获取元素相对于屏幕左上角的 X 和 Y 坐标 const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离 const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离 return [x, y]; }
onMounted(() => { createBroadcastChannel(); resizeEventBind(); }); return {}; } }; </script> <style lang="scss"></style> 这里,我们的核心逻辑在于:
在 这样,当我们同时打开两个窗口,移动其中一个窗口,就可以向另外一个窗口发生当前窗口希望传递过去的信息,在本例子中就是 假设 可以看到,如果我们同时打开两个一个的页面,当触发右边页面的 Resize,左边的页面会收到基于 而一个完整的 Event 信息如下: 譬如,传递过来的信息放在 data 属性内、同时也可以获取当前的的 Broadcast Name 等。 基于 BroadcastChannel,就可以实现每个 Tab 内的核心信息互传, 可以得知当前在线设备数,再基于这些信息去完成我们想要的动画、交互等效果。 这里的核心点,还是:
其本质就是一个数据共享池子。 方式二:SharedWorker API好,介绍完 Broadcast Channel(),我们再来看看 SharedWorker API。 SharedWorkerAPI 是 HTML5 中提供的一种多线程解决方案,它可以在多个浏览器 TAB 页面之间共享一个后台线程,从而实现跨页面通信。 与其他 Worker 不同的是,SharedWorker 可以被多个浏览器 TAB 页面共享,且可以在同一域名下的不同页面之间建立连接。这意味着,多个页面可以通过 SharedWorker 实例之间的消息传递,实现跨 TAB 页面的通信。 它的实现与上面的 Broadcast Channel 非常类似,我们来看一看实际的代码: <template> <div id="j-main"> // ... </div> </template> <script> import { onMounted } from 'vue'; export default { setup() { // 创建一个 SharedWorker 对象 let worker;
function initWorker() { // 创建一个 SharedWorker 对象 worker = new SharedWorker('/shared-worker.js', 'tabWorker'); // 监听消息事件 worker.port.onmessage = function (event) { console.log('接收到 event', event); handleMessage(event); }; }
function handleMessage(data) { // TODO: 处理接收到信息后的逻辑 } function sendMessage(data) { // 发送消息 worker.port.postMessage(data); } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); sendMessage(pos); }); } function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 获取元素相对于屏幕左上角的 X 和 Y 坐标 const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离 const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离 return [x, y]; }
onMounted(() => { initWorker(); resizeEventBind(); }); return {}; } }; </script> <style></style> 简单描述一下,上面也说了,跨 Tab 页通信的核心在于数据向外的发送与接收的能力:
当然,上面有引入一个 //shared-worker.js const connections = []; onconnect = function (event) { var port = event.ports[0]; connections.push(port); port.onmessage = function (event) { // 接收到消息时,向所有连接发送该消息 connections.forEach(function (conn) { if (conn !== port) { conn.postMessage(event.data); } }); }; port.start(); }; 简单解析一下,下面对其进行解析:
总而言之,shared-worker.js 脚本创建了一个共享 Worker 实例,它可以接收来自不同页面的连接请求,并将接收到的消息发送给其他连接的页面。通过使用 SharedWorker API,实现跨 TAB 页面之间的通信和数据共享。 同理,我们来看看基于 Worker 的数据传输效果,同样是简化 DEMO,当 Resize 窗口时,向另外一个窗口发送当前窗口下 可以看到,如果我们同时打开两个一个的页面,当触发右边页面的 Resize,左边的页面会利用 而一个完整的 Event 信息如下: 可以看到,在 SharedWorker 方式中,传输数据与 Broadcast Channel 是一样的,都是利用
兼容性方面,到今天(2023-11-26),broadcast Channel 看着是兼容性更好的方式: 另外,需要注意的是,两个方法都使用了 但是,单独使用 方式三:localStorage/sessionStorageOK,最后一种跨 Tab 窗口通信的方式是利用 与上面 Broadcast Channel、SharedWorker 稍微不同的地方在于:
简单看看代码: <template> <div id="j-main"> // ... </div> </template> <script> import { ref, reactive, computed, onMounted } from 'vue'; export default { setup() { function initLocalStorage() { let tabArray = JSON.parse(localStorage.getItem('tab_array')); if (!tabArray) { const tabIndex = 1; id = tabIndex; localStorage.setItem('tab_array', JSON.stringify([tabIndex])); } else { const tabIndex = tabArray[tabArray.length - 1] + 1; id = tabIndex; const newTabArray = [...tabArray, tabIndex]; localStorage.setItem('tab_array', JSON.stringify(newTabArray)); } } function setLocalStorage(data) { localStorage.setItem(`tab_index_${id}`, JSON.stringify(data)); } function handleMessage(data) { const rArray = JSON.parse(data); remoteX.value = rArray[0]; remoteY.value = rArray[1]; } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); setLocalStorage(pos); });
window.addEventListener('storage', (event) => { console.log('localStorage 变化了!', event); console.log('键名:', event.key); console.log('变化前的值:', event.oldValue); console.log('变化后的值:', event.newValue); handleMessage(event.newValue); }); } function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 获取元素相对于屏幕左上角的 X 和 Y 坐标 const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离 const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离 return [x, y]; }
onMounted(() => { initLocalStorage(); resizeEventBind(); }); return {}; } }; </script> <style></style> 同样的简单解析一下:
交互传输结果,与上述两个动图是一致的,就不额外贴图了,但是基于 我们通过 当然,由于 虽然看起来这种方式最不优雅,但是结合兼容性一起看, localstorage 反而是兼容性最好的方式。在数据量较小的时候,性能相差不会太大,反而可能是更好的选择。 我基于上面三种方式:Broadcast Channel、SharedWorker 与 localStorage,都实现了一遍下面这个跨 Tab 页的 CSS 联动动画: 三种方式的代码都不多,感兴趣的可以戳这里:Github - broadcastAnimation 实际应用思考当然,上面的实现其实有很大一个瑕疵。 那就是我们只顾着实现通信,没有考虑实际应用中的一些实际问题:
基于实际应用,我们需要基于上述 3 种方式,进一步细化方案。 上面,为了方便演示,每次传输数据时,只传输动画需要的数据。而实际应用,我们可以需要细化整个传输数据,设定合理的协议。譬如: { // 传输状态: // 1 - 首次传输 // 2 - 正常通信 // 3 - 页面关闭 status: 1 | 2 | 3, data: {} } 接收方需要基于收到信息所展示的不同的状态,做出不同的反馈。 当然,还有一个问题,我们如何知道页面被关闭了?基于组件的 这些信息都有可能因为 Tab 页面失活,导致关闭的信息无法正常被发送出去。所以,实际应用中,我们经常用的一项技术是心跳上报/心跳广播,一旦建立连接后,间隔 X 秒发送一次心跳广播,告诉其他接收端,我还在线。一旦超过某个时间阈值没有收到心跳上报,各个订阅方可以认为该设备已经下线。 总而言之,跨 Tab 窗口通信应用在实际应用的过程中,我们需要思考更多可能隐藏的问题。 跨 Tab 窗口通信应用场景当然,除了最近大火的跨 Tab 动画应用场景,实际业务中,还有许多场景是它可以发挥作用的。这些场景利用了跨 Tab 通信技术,增强了用户体验并提供了更丰富的功能。 以下是一些常见的应用场景:
譬如这个:
举几个实际的例子:
总之,跨 Tab 窗口通信在实时协作、数据同步、通知提醒等方面都能发挥重要作用,为用户提供更流畅、便捷的交互体验。 最后本文只罗列了 3 种较为常见,适用性强的方式。除去本文罗列的方式,肯定还有其他方式能够实现跨 Tab 通信。 譬如,基于 Window: opener property 配合 更多有意思的方式,期待大家的补充与探索。 好了,本文到此结束,希望对你有帮助 😃 作者:ChokCoco 来源链接:https://www.cnblogs.com/coco1s/p/17861360.html 该文章在 2023/11/28 15:03:45 编辑过 |
关键字查询
相关文章
正在查询... |