以至于,基于这个效果的二次创作层出不穷,眼花缭乱。
基于跨窗口通信的弹弹球:
基于跨窗口通信的 Flippy Bird:
我也考试测验制作了一个跨 Tab 窗口的 CSS 动画联动,效果如下:
代码不多,核心代码 200 行,感兴趣的可以戳这里:Github - broadcastAnimation
当然,本文的核心不是去逐一阐发上面的效果详细的实现办法,而是讲讲个中比较关键的一个技能点:
而是运用如何在多窗口下进行相互通信。
所谓多窗口下进行相互通信,是指在浏览器中,不同窗口(包括不同标签页、不同浏览器窗口乃至不同浏览器实例)之间进行数据传输和通信的能力。
当然,本文我们磋商的是纯前真个跨 Tab 页面通信,在非纯前真个办法下,我们可以借助诸如 Web Socket 等办法,藉由后端这个中间载体,进行跨页面通信。
因此,本文我们更多的重心将放在,如何基于纯前端技能,实现多窗口下进行相互通信。
为了实现跨窗口通信,它该当须要具备以下能力:
数据传输能力:能够将数据从一个窗口发送到另一个窗口,以及吸收来自其他窗口的数据。实时性:能够实现实时或近实时的数据传输,以便及时更新不同窗口的内容。安全性:确保通信过程中的数据安全,防止恶意盗取或修改通信数据。当然,这个不是本文谈论的重点,但是是实际运用中不应该忽略的一个重点。办法一:Broadcast Channel()Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,许可一个窗口发送,并由其他窗口吸收。
其核心步骤如下:
创建一个 BroadcastChannel 工具:在发送和吸收之前,首先须要在每个窗口中创建一个 BroadcastChannel 工具,利用相同的频道名称进行初始化。发送:通过 BroadcastChannel 工具的 postMessage() 方法,可以向频道中的所有窗口发送。吸收:通过监听 BroadcastChannel 工具的 message 事宜,可以在窗口中吸收到来自其他窗口发送的。同时,Broadcast Channel 遵照浏览器的同源策略。这意味着只有在同一个协议、主机和端口下的窗口才能正常进行通信。如果窗口不知足同源策略,将无法相互发送和吸收。
由于有同源限定,我们须要起一个做事,这里我基于 Vite 快速起了一个 Vue 项目,大略的基于 .vue 文件下进行一个演示。
其核心代码非常大略:
<template> <div class="g-container" 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>
这里,我们的核心逻辑在于:
createBroadcastChannel() 函数用于创建一个 BroadcastChannel 工具,并设置处理函数。sendMessage(data) 函数用于向 BroadcastChannel 发送。handleMessage(event) 函数用于处理吸收到的。resizeEventBind() 函数用于监听窗口大小变革事宜,并在事宜发生时获取当前元素的位置信息,并通过 sendMessage() 函数发送位置信息到 BroadcastChannel。getCurPos() 函数用于打算当前元素相对付显示器窗口右上角的间隔。在 onMounted() 生命周期钩子中,调用了 createBroadcastChannel() 和 resizeEventBind() 函数,用于在组件挂载后实行干系的初始化操作。
这样,当我们同时打开两个窗口,移动个中一个窗口,就可以向其余一个窗口发生当前窗口希望通报过去的信息,在本例子中便是 #j-main 元素间隔显示器右上角的间隔。
假设 #j-main 只是一个在浏览器正中央矩形,我们同时打开两边的掌握台,看看会发生什么:
可以看到,如果我们同时打开两个一个的页面,当触发右边页面的 Resize,左边的页面会收到基于 broadcastChannel.onmessage = handleMessage 吸收到的信息,反之同理。
而一个完全的 Event 信息如下:
譬如,通报过来的信息放在 data 属性内、同时也可以获取当前的的 Broadcast Name 等。
基于 BroadcastChannel,就可以实现每个 Tab 内的核心信息互传, 可以得知当前在线设备数,再基于这些信息去完成我们想要的动画、交互等效果。
这里的核心点,还是:
数据向其他 Tab 页面通报的能力Tab 页面接管其他页面通报过来的数据的能力实在质便是一个数据共享池子。
办法二:SharedWorker API好,先容完 Broadcast Channel(),我们再来看看 SharedWorker API。
SharedWorker API 是 HTML5 中供应的一种多线程办理方案,它可以在多个浏览器 TAB 页面之间共享一个后台线程,从而实现跨页面通信。
与其他 Worker 不同的是,SharedWorker 可以被多个浏览器 TAB 页面共享,且可以在同一域名下的不同页面之间建立连接。这意味着,多个页面可以通过 SharedWorker 实例之间的通报,实现跨 TAB 页面的通信。
它的实现与上面的 Broadcast Channel 非常类似,我们来看一看实际的代码:
<template> <div class="g-container" 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 lang="scss"></style>
大略描述一下,上面也说了,跨 Tab 页通信的核心在于数据向外的发送与吸收的能力:
initWorker() 方法中,利用 worker = new SharedWorker('/shared-worker.js', 'tabWorker') 创建了一个 SharedWorker , 后面每一个被打开的同域浏览器 TAB 页面,都是共享这个 Worker 线程,从而实现跨页面通信基于 worker.port.postMessage(data)实现数据的传输基于 worker.port.onmessage = function() {} 实现传输数据的监听当然,上面有引入一个 /shared-worker.js,这个是须要额外定义的,一个极简版本的代码如下:
//shared-worker.jsconst 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();};
大略解析一下,下面对其进行解析:
上面的代码中,定义了一个数组 connections,用于存储与 SharedWorker 建立连接的各个页面的端口工具;onconnect 是事宜处理程序,当有新的连接建立时会触发该事宜;在 onconnect 函数中,通过 event.ports[0] 获取到与 SharedWorker 建立的连接的第一个端口工具,并将其添加到 connections 数组中,表示该页面与共享 Worker 建立了连接。在连接建立后,为每个端口工具设置了 onmessage 事宜处理程序。当端口工具吸收到时,会触发该事宜处理程序。在 onmessage 事宜处理程序中,通过遍历 connections 数组,将发送给除当前连接端口工具之外的所有连接。这样,就可以在不同的浏览器 TAB 页面之间通报。末了,通过调用 port.start() 启动端口工具,使其开始吸收。总而言之,shared-worker.js 脚本创建了一个共享 Worker 实例,它可以吸收来自不同页面的连接要求,并将吸收到的发送给其他连接的页面。通过利用 SharedWorker API,实现跨 TAB 页面之间的通信和数据共享。
同理,我们来看看基于 Worker 的数据传输效果,同样是简化 DEMO,当 Resize 窗口时,向其余一个窗口发送当前窗口下 #j-main 元素的坐标:
可以看到,如果我们同时打开两个一个的页面,当触发右边页面的 Resize,左边的页面会利用 worker.port.onmessage = function() {} 收到基于 worker.port.postMessage(data) 发送的信息,反之同理。
而一个完全的 Event 信息如下:
可以看到,在 SharedWorker 办法中,传输数据与 Broadcast Channel 是一样的,都是利用 Message Event。大略比拟一下:
SharedWorker 通过在多个Tab页面之间共享相同的 Worker 实例,方便地共享数据和状态,SharedWorker 须要多定义一个 shared-worker.js;Broadcast Channel 通过向所有订阅同一频道的 Tab 页面广播,实现广播式的通信。兼容性方面,到本日(2023-11-26),broadcast Channel 看着是兼容性更好的办法:
其余,须要把稳的是,两个方法都利用了 postMessage 方法。window.postMessage() 方法可以安全地实现跨源通信。并且,实质上而言,单独利用 postMessage 就可以实现跨 Tab 通信。
但是,单独利用 postMessage 适宜大略的点对点通信。在更繁芜的场景中,Broadcast Channel 和 SharedWorker 供应更强大的机制,可简化通信逻辑,有更广泛的通信范围和生命周期管理。Broadcast Channel 的通信范围是所有订阅该频道的窗口,而 SharedWorker 可在多个窗口之间共享状态和通信。
办法三:localStorage/sessionStorageOK,末了一种跨 Tab 窗口通信的办法是利用 localStorage 、sessionStorage 本地化存储 API 以及的 storage 事宜。
与上面 Broadcast Channel、SharedWorker 轻微不同的地方在于:
localStorage 办法,利用了本地浏览器存储,实现了同域下的数据共享;localStorage 办法,基于 window.addEventListener('storage', function(event) {})事宜实现了 localStore 变革时候的数据监听;大略看看代码:
<template> <div class="g-container" 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 lang="scss"></style>
同样的大略解析一下:
每次页面初始化时,都会首先有一个 initLocalStorage 过程,用于给当前页面一个唯一 ID 标识,并且存入 localStorage 中每次页面 resize,将当前页面元素 #j-main 的坐标值,通过 ID 标识当 Key,存入 localStorage 中其他页面,通过 window.addEventListener('storage', (event)=> {}) 监听 localStorage 的变革交互传输结果,与上述两个动图是同等的,就不额外贴图了,但是基于 storage 事宜传输的值有点不一样,我们展开看看:
我们通过 window.addEventListener('storage', (event)=> {}) ,可以拿到这次变革的 localStorage key 是什么,前值 oldValue 与 newValue 等等。
当然,由于 localStorage 存储过程只能是字符串,在读取的时候须要利用 JSON.stringify 和 JSON.parse 额外处理一层,调试的时候须要把稳。
虽然看起来这种办法最不优雅,但是结合兼容性一起看, localstorage 反而是兼容性最好的办法。在数据量较小的时候,性能相差不会太大,反而可能是更好的选择。
我基于上面三种办法:Broadcast Channel、SharedWorker 与 localStorage,都实现了一遍下面这个跨 Tab 页的 CSS 联动动画:
三种办法的代码都不多,感兴趣的可以戳这里:Github - broadcastAnimation
实际运用思考当然,上面的实现实在有很大一个瑕疵。
那便是我们只顾其实现通信,没有考虑实际运用中的一些实际问题:
如何确定何时开始通信?Tab 页频繁的开关,如何知道当前还有多少页面处于打开状态?基于实际运用,我们须要基于上述 3 种办法,进一步细化方案。
上面,为了方便演示,每次传输数据时,只传输动画须要的数据。而实际运用,我们可以须要细化全体传输数据,设定合理的协议。譬如:
{ // 传输状态: // 1 - 首次传输 // 2 - 正常通信 // 3 - 页面关闭 status: 1 | 2 | 3, data: {}}
吸收方须要基于收到信息所展示的不同的状态,做出不同的反馈。
当然,还有一个问题,我们如何知道页面被关闭了?基于组件的 onUnmounted 发送当前页面关闭的信息或者基于 window 工具的 beforeunload 事宜发送当前页面关闭的信息?
这些信息都有可能由于 Tab 页面失落活,导致关闭的信息无法正常被发送出去。以是,实际运用中,我们常常用的一项技能是心跳上报/心跳广播,一旦建立连接后,间隔 X 秒发送一次心跳广播,见告其他吸收端,我还在线。一旦超过某个韶光阈值没有收到心跳上报,各个订阅方可以认为该设备已经下线。
总而言之,跨 Tab 窗口通信运用在实际运用的过程中,我们须要思考更多可能隐蔽的问题。
跨 Tab 窗口通信运用处景当然,除了最近大火的跨 Tab 动画运用处景,实际业务中,还有许多场景是它可以发挥浸染的。这些场景利用了跨 Tab 通信技能,增强了用户体验并供应了更丰富的功能。
以下是一些常见的运用处景:
实时协作:多个用户可以在不同的 Tab 页上进行实时协作,比如编辑文档、共享白板、协同编辑等。通过跨Tab通信,可以实现实时更新和同步操作,提高协作效率。譬如这个:
多标签页数据同步:当用户在一个标签页上进行了操作,希望其他标签页上的数据也能实时更新时,可以利用跨 Tab 通信来实现数据同步,保持用户在不同标签页上看到的数据同等性。跨标签页关照:在某些场景下,须要向用户发送关照或提醒,即利用户不在当前标签页上也能及时收到。通过跨 Tab 通信,可以实现跨页面的通报,向用户发送关照或提醒。多标签页状态同步:有些运用可能须要在不同标签页之间同步用户的状态信息,例如登录状态、购物车内容等。通过跨 Tab 通信,可以确保用户在不同标签页上看到的状态信息保持同等。页面间数据传输:有时候用户须要从一个页面跳转到另一个页面,并携带一些数据,通过跨Tab通信可以在页面之间通报数据,实现数据的共享和通报。
举几个实际的例子:
某系统是一个国际化电商的仓库管理系统,系统能切换到环球各地不同的仓库进行数据操作,当用户打开了页面后,又新开了一个 Tab 页面,并且切换到其余一个仓库进行操作。当用户重新回到第一个打开的页面时,为了防止用户缺点操作数据(前端界面是同等的,可能忘却了自己切换过仓库),通过弹窗提醒用户你已经切换过仓库;某音乐播放器 PC 页面,在列表页面进行歌曲播放点击,如果当前没有音乐播放详情页,则打开一个新的播放详情页。但是,如果页面已经存在一个音乐播放详情页,则不会打开新的音乐播放详情页,而是直策应用已经存在的播放详情页面;系统有与列表页与内容页,在内容页点击已阅,如果用户同时打开了上级列表页,要取消列表页关于该内容页的未读的提示;总之,跨 Tab 窗口通信在实时协作、数据同步、关照提醒等方面都能发挥主要浸染,为用户供应更流畅、便捷的交互体验。
末了本文只罗列了 3 种较为常见,适用性强的办法。撤除本文罗列的办法,肯定还有其他办法能够实现跨 Tab 通信。
譬如,基于 Window: opener property 合营 postMessage 也可以实现跨 Tab 窗口通信,但是这种通信仅仅适用于当前窗口以及通过当前窗口新开的窗口之间的通信。
更多故意思的办法,期待大家的补充与探索。
好了,本文到此结束,希望对你有帮助 :)
如果还有什么疑问或者建议,可以多多互换,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望奉告。