多种标签之间数据通信实现方式
一、前言
浏览器的每个标签是独立存在的,也无法直接共享数据;但在少数场景下,需求要求在不同的标签页之间进行数据传输和数据通信,这就需要使用跨标签的数据通信来实现功能需求。
常用的标签🏷之间的实现方案:
- BroadCast Channel
- window.open + window.postMessage
- WebSocket
- 路由 Query
- LocalStorage window.onstorage
- Cookie + SetInterval
- Server Worker
- Shared Worker
- IndexedDB + setInterval
二、Example
2.1 BroadcastChannel
BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。
申请者获取数据:
constapplyChannel=newBroadcastChannel("openApi-BChannel-apply");// 申请者通道constproviderChannel=newBroadcastChannel("openApi-BChannel-provider");// 提供者通道constuniqueIcon="openapi-dosc#"+Math.random();applyChannel.postMessage({uniqueIcon});// 关闭申请者通道setTimeout(()=>{applyChannel.close();},0);providerChannel.onmessage=(event)=>{if(event.target.name==="openApi-BChannel-provider"&&event.data.uniqueIcon===uniqueIcon){debugger;// todoproviderChannel.close();}};提供者发送数据:
constapplyChannel=newBroadcastChannel("openApi-BChannel-apply");// 申请者通道constproviderChannel=newBroadcastChannel("openApi-BChannel-provider");// 提供者通道constprivateData={money:"$9999",};applyChannel.onmessage=(event)=>{// 判断专属通道if((event.target.name="openApi-BChannel-apply")){providerChannel.postMessage({privateData,uniqueIcon:event.data.uniqueIcon,});// 任务完成,关闭通道setTimeout(()=>{applyChannel.close();providerChannel.close();});}};为了之间的通信安全,我们在这儿预置实例化了申请者和提供者两个通信通道,申请方先走专属的申请者通道(‘openApi-BChannel-apply’)去向提供方申请需要经费,并提供申请方标识(uniqueIcon);提供方确认是己方(‘openApi-BChannel-apply’),提供方准备好经费后,走专属的提供则通道下发给申请者。
申请者接收到包裹后,检验是否是从专属的提供者通道过来的,并确认是否是自己的,再勉为其难地收入囊中。
记得在组件中使用的时候,要及时的关闭申请者通道,如果其他申请者也不需要数据时,提供者实例通道也要关闭。
2.2 window.open()&window.postMessage()
- window.open(): Window 接口的 open() 方法,是用指定的名称将指定的资源加载到新的或已存在的浏览上下文(标签、窗口或 iframe)中。
- window.postMessage(): window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
提供者通过 window.open()去新建标签页,等新建标签页加载完成后,触发 onload 事件,给新标签页发送信息
constnewTabDebug=window.open("#/debug","_blank");newTabDebug.onload=()=>{newTabDebug.postMessage({currNode:"$9999"},"*");};
申请者监听 message 事件接受到信息
window.addEventListener("message",function(event){console.log(event.data&&event.data.currNode);});2.3 WebSocket
WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
(1)在 Server 启用 Service Worker
npm install-Sws// 安装 ws// 获得WebSocketServer类型letWebSocketServer=require("ws").Server;// 创建WebSocketServer对象实例,监听指定端口letserver=newWebSocketServer({port:8000});// 创建保存所有已连接到服务器的客户端对象的数组letclients=[];// 为服务器添加 connection 事件监听,当有客户端连接到服务端时,立刻将客户端对象保存进数组中。server.on("connection",function(client){if(clients.indexOf(client)===-1){clients.push(client);console.log("有"+clients.length+"个客户端在线");// 为每个 client 对象绑定 message 事件,// 当某个客户端发来消息时,自动触发client.on("message",function(msg){console.log(msg.data);// 遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端for(letkeyofclients){if(key!=client){key.send(msg.data);}}});}});// 启动服务器的命令 node server.js(2) 提供者建立链接 🔗,发送数据
constws=newWebSocket("ws://localhost:8000");//创建Wevsocket实例化ws.onerror=function(){console.log("失败了");//建立失败的函数};constdata={id:newDate().getTime(),user:"fx",dateTime:newDate().getTime(),msg:"",};// 当连接建立成功,触发 open 事件ws.onopen=function(evt){console.log("建立连接成功 ...");// 连接建立成功以后,就可以使用这个连接对象通信了// send 方法发送数据// ws.send(JSON.stringify(data));};当建立链接后,会触发 webSocket 的实例的 onpen 事件
此时 server 会接受到链接申请:
(3)申请者
通过 onmessage API 监听(新建标签)
varws=newWebSocket("ws://localhost:8000");//创建Wevsocket实例化ws.onmessage=(e)=>{console.log(e.data);debugger;};> 到这里,提供者 -> server -> 申请者 之间的通信就建立起来了,这时,提供者发送数据
ws.send(JSON.stringify({name:"晓东",age:22,money:"$99"}));此时 Server 端收到的是 Buffer 类型的二进制格式数据流,而申请者收到的是 Blob 的对象,那是因为 WebSocket 是通过二进制流进行数据通信的,这就需要在 Server 将 Buffer 转为其他数据类型(比如:String 或 Object)
client.on("message",function(msg,isBinary){msg=isBinary?msg:msg.toString();console.log(msg);console.log(isBinary);// 遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端for(letkeyofclients){if(key!=client){key.send(msg);}}});再发送数据时:
ws.send(JSON.stringify({name:"小明",age:22,money:"$9999"}));ws.send("hahhaha");2.4 路由 Query
通过 window.open(url, target, windowFeatures)打开新标签,在新标签初始化完成的函数里,通过 location.href 拿到 UrlParams,再将参数解析出来即可。
解析 UrlParams:
functionparseParam(url){constparamsStr=/.+\?(.+)$/.exec(url)[1];// 将" ?"后面的字符串取出来constparamsArr=paramsStr.split("&");// 将字符串以"&"分隔后存在数组letparamsObj={};// 将params存到对象中paramsArr.forEach((param)=>{// 处理有value的参数if(/=/.test(param)){let[key,val]=param.split("=");// 分割key和valueval=decodeURIComponent(val);// 解码val=/^\d+$/.test(val)?parseFloat(val):val;if(paramsObj.hasOwnProperty(key)){// 如果对象有key,则添加一个值paramsObj[key]=[].concat(paramsObj[key],val);}else{// 如果对象没有这个key,创建key并设置值paramsObj[key]=val;}}else{// 处理没有value的参数paramsObj[param]=true;}});returnparamsObj;}// testvarurl="https://easylearn.baidu.com/edu-page/tiangong/questiondetail?id=1711614621853513447&fr=search";parseParam(url);2.5 LocalStorage window.onstorage
localStorage:只读的 LocalStorage 属性允许你访问一个 Document 源(origin)的对象 Storage;存储的数据将保存在浏览器会话中。localStorage 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束——也就是说,当页面被关闭时,存储在 sessionStorage 的数据会被清除。
** window.onstorage:** 当存储区域(localStorage 或 sessionStorage)被修改时,将触发 storage 事件。
提供者通过 localStorage.setItem()更新数据:
localStorage.setItem("we_are_famify",JSON.stringify({target:"fater",data:{money:"$9999"}}));
申请者通过 window.onstorage 监听事件来获取数据:
window.onstorage=function(event){debugger;if(event.key==="we_are_famify"){console.log(event.newValue);}};2.6 Cookie + SetInterval
获取并设置与当前文档相关联的 cookie。可以把它当成一个 getter and setter。
提供者将数据存入到 Cookie:
document.cookie="money=$9999";申请者通过轮询来查阅 document.cookie 中是否有自己的字段。当查阅到自己所需的字段有值时,清除掉自己的字段和定时器:
consttimer=setInterval(function(){// 获取存储在 Cookie 中的消息constcookies=document.cookie.split(";");letmessage="";for(leti=0;i<cookies.length;i++){constcookie=cookies[i].trim();if(cookie.startsWith("money=")){message=cookie.substring("money=".length);break;}}// 处理消息if(message){console.log("Received money:",decodeURIComponent(message));// 清除 Cookie和定时器document.cookie="money=; expires=Thu, 01 Jan 1999 00:00:00 UTC; path=/;";clearInterval(timer);}},1000);2.7 Server Worker
Service Worker API 的 ServiceWorker 接口提供了对 service worker 的引用。各个浏览上下文(例如页面、worker 等)可以与相同的 service worker 相关联,每个浏览上下文都可以通过唯一的 ServiceWorker 对象访问。
详细原理解析可以 reading:https://juejin.cn/post/7283313180729573395?searchId=20231204200524780957E4868B5A5CDB55。个人觉得很Nice~ 的一片好文。
跟 webSocket 类似,也需要另外侦听注册实例添加事件,但不用另外起借助 node 或者其他工具 run 后台服务,
sw.js:,在这儿,我将 ws.js 视为一个通信的工具,所以,我将该文件存放在静态资源(public 目录下)。
self.addEventListener("message",async(event)=>{constclients=awaitthis.clients.matchAll();clients.forEach(function(client){client.postMessage(event.data);});});self.addEventListener("activate",(event)=>{clients.claim();console.log("Ready!");});其中的 activate 事件,是为了 controller 不为 null。
提供者: 需要通过 sw.js 来注册,使用 ServiceWorker.Container.register() 方法首次注册 service worker。如果注册成功,service worker 就会被下载到客户端并尝试安装或激活,这将作用于整个域内用户可访问的 URL,或者其特定子集。最好在入口页 index.html 来进行注册;也可以在提供者页初始化时进行注册【发送信息之前注册都可以,一次就好~】,就可以用 navigator.serviceWorker.controller.postMessage()去发送信息了。
<!--注册-->navigator.serviceWorker.register('/sw.js').then(()=>{console.log('service worker 注册成功');});<!--发送信息-->constmsg={money:'$999',yourAge:22,from:'提供者',to:'申请者',}navigator.serviceWorker.controller.postMessage(JSON.stringify(msg));console.log('我给你一个礼物')申请者: 在 web 项目的任何地方使用 navigator.serviceWorker.onmessage 来获得消息,如果是多个信息交叉通信,也可以同构 Name 或者 origin 来识别是不是自己所需要的消息。
navigator.serviceWorker.onmessage=function({data}){console.log(JSON.parse(data));debugger;};2.8 Shared Worker
SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域。
跟 Sever Worker 相似,但又有些不一样,先上代码
同样的,第一步先注册:
sw.js:
// 监听来自主页面的消息self.onconnect=function(event){constport=event.ports[0];// 监听来自主页面的消息port.onmessage=function(event){console.log("Received message from main page:",event.data);// 向主页面发送消息port.postMessage(event.data);};};// 在主页注册,并挂在到window对象constworker=newSharedWorker("/sw.js");window.worker=worker;申请者:
window.worker.port.onmessage=function(event){console.log("申请者收到你的礼物",JSON.parse(event.data));};提供者:
constmsg={money:"$999",yourAge:22,from:"提供者",to:"申请者"};window.worker.port.postMessage(JSON.stringify(msg));2.9 IndexedDB + setInterval
indexedDB 是 WindowOrWorkerGlobalScope 的一个只读属性,它集成了为应用程序提供异步访问索引数据库的功能的机制。
特点
- 键值对储存 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
- 异步 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 支持二进制储存 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象。
储存空间大 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。储 存 在 电 脑 上 中 的 位 置 为 C:\Users\当 前 的 登 录 用 户\AppData\Local\Google\Chrome\User Data\Default\IndexedDB
应用indexedDB进行标签之间的通信,原理相似于LocalStorage + setInterval,将数据存储,再轮询indexedDB,获取到数据。
但浏览器对其的兼容不太友好(eg:IE),也由于其存储量大的原因,也称其数据库,并且是存储在系统盘,一旦数据存储的数据量大时,用户在浏览器使用就会觉得莫名其妙的把盘给占满(流氓行为);而且前端一般也不会存储大量的数据;但既然indexedDB面世,也是有它的用武之地的。比如,在网站文章的富文本编辑器,要求有返回上一步功能,草稿功能;股票金融类的统计图数据,数据是实时更新的,可以把已经加载过的数据缓存到本地,再次查询指定时间段的时候就减少后台压力了;当然这些场景使用本地缓存不一定是最佳方案,但是有indexedDB确实是可以优化业务的。相对于localstorage而言,这绝对是前端存储的扛把子的存在,也正是因为如此,其学习成本和使用成本要相对而言较高,不亚于学习数据库的操作指令(有兴趣的伙伴可以去尝试一下Node + express + MongoDB,搭建简易的后台系统,尝尝鲜);也存在读写事务互相阻塞,批量操作数据读写时,相较于慢。
既然已经吹到前端的存储了,那就顺便整理一下前端常用的存储特点:
三、总结
- BroadCast Channel
优点:
- 第一次打开,之后再传参无需刷新界面重新加载资源;
- 可以同时使用多个频道或共用一个频道,通信简单;
缺点:
受同源策略限制,只会向同频道的同源页面发送或接受,但也是其优势.
- LocalStorage window.onstorage
优点:
- 第一次打开之后,再传参无需刷新新界面重新加载资源;
- 浏览器兼容性好,操作简单便捷;
缺点:
- 受同源策略限制,LocalStroage 需在同源策略的前提下,页面才能共享;
- 需要通过 LocalStroage 检控通信,控制相对麻烦,复杂场景使用不便捷;
- 无痕环境下或部分浏览器隐身,无法使用设置 localStorage(eg:Safari),就如 H5 无法设置 cookie,从而无法触发到 onstrage。
- window.open + window.postMessage
- Service Worker
- Shared Worker
优点:理论上可以实现任何数据共享性能较好;
缺点:跨域不共享调试不方便兼容性不好。
- cookie + SetInterval
优点:兼容性好易于上手和理解;
缺点:有存储大小限制轮询消耗性能发请求会携带 cookie。
- Websocket
优点:使用灵巧,便捷且功能强大,属于服务端给予的一个外挂;
缺点:
- 需要借助服务端的支持,如果通信数据量大时,比较消耗服务端资源,但又有什么两者兼得的办法嘞;
- 必须在服务端添加监听。
- IndexedDB+ SetInterval
结言:之所以会有这么多实现方式,即是技术更迭发展的足迹,也是程序猿鬼才般地实现手段;能够借助外挂法,缓存等非专用通信方式来与其他的手段结合实现同等通信效果;不禁感叹,程序猿的奇思妙想,代码世界的多姿多彩!这些通信方式各有优劣,选择适合的方式取决于具体的需求和场景,选择合适的通信方式来实现跨标签页的通信。