target当前页面需要与浏览器中当前打开的一个标签页进行通信,完成一些交互。其中,与当前页面通信的标签页可以与当前页面同域(同协议、同域名、同端口),也可以跨域。要实现这一特殊功能,不可能只使用HTML5的相关特性,需要更巧妙的设计。想象现在我们发现我们的思路,在各种场景下假设解决方案,最后找到一个通用的解决方案。情况1,两个需要交互的tab页有依赖关系。例如,通过A页面中JavaScript的window.open打开B页面,或者通过iframe将B页面嵌入到A页面中。这种情况最简单,可以通过HTML5的window.postMessageAPI来完成通信,因为postMessage函数绑定了window全局对象,所以其中一个通信页面(比如页面A)必须能够获取到另一个页面(如B页面)的window对象,从而完成单向通信;B页面不需要获取A页面的window对象,如果要从B页面通信到A页面,只需要在B页面监听message事件,获取事件中传入的source对象,即引用页面A上的窗口对象:页面Bwindow.addEventListner('message',(e)=>{let{data,source,origin}=e;source.postMessage('messageecho','/');});postMessage的第一个参数是消息实体,它是一个结构化对象,即可以通过“JSON.stringify和JSON.parse”函数还原的对象;第二个参数是消息发送范围选择器,设置为“/”表示只向同源页面发送消息,设置为“*”表示发送所有页面。在情况2中,打开的两个页面属于同源类别。要实现两个不相关的源标签页之间的通信,可以使用更巧妙的方式:localstorage。localStorage的存储遵循同源策略,所以同源的两个标签页可以通过这种共享localStorage的方式进行通信。通过约定localStorage的某个itemName,将基于key值的内容作为“共享硬盘”进行通信。但是,如果单纯的使用localStorage存储作为通信方式,就会遇到一个问题,就是两个页面无法掌握通信的时机。如果此时A页面需要向B页面发送消息“helloB”,则会设置localStorage.setItem('message','helloB'),使用setTimeout轮训等待B的消息;而B也使用setTimeout轮训等待此时localStorage消息项的变化。当获取到'message'字段时,取出消息'helloB'。如果B要给A发消息,还是用同样的套路。这种方式性能极低,需要通信双方不断监听localStorage某一项的变化,浪费了事件队列的处理效率。好在HTML5提供了storage事件,通过window对象监听storage事件,监听localStorage对象的change事件(包括item的增加、修改和删除)。因此,可以通过事件完成一个高效的通信机制:一个页面window.addEventListener("storage",function(ev){if(ev.key=='message'){//removeItem也会触发storage事件,此时timeev.newValue为空if(!ev.newValue)return;varmessage=JSON.parse(ev.newValue);console.log(message);}});functionsendMessage(message){localStorage.setItem('message',JSON.stringify(消息));localStorage.removeItem('message');}//发送消息给页面BsendMessage('thisismessagefromA');pageBwindow.addEventListener("storage",function(ev){if(ev.key=='message'){//removeItem也会触发storage事件,此时ev.newValue为空if(!ev.newValue)return;varmessage=JSON.parse(ev.newValue);//向页面A发送消息sendMessage('messageechofromB');}});functionsendMessage(message){localStorage.setItem('message',JSON.stringify(消息));localStorage.removeItem('消息');}要发送消息,使用sendMessage函数,该函数将消息序列化,将其设置为localStorage的消息字段的值,然后删除消息字段。这样做的目的不是为了污染localStorage空间,而是会引起一个无害的反应,即触发两次storage事件,所以我们做了if(!ev.newValue)return;存储事件处理函数中的判断。当我们在A页面执行sendMessage函数时,其他同源页面会触发storage事件,但是A页面不会触发storage事件;并且连续两次发送同一条消息只会触发一次存储事件,如果需要解决这个问题的话,可以在消息体中添加时间戳:sendMessage({data:'helloworld',timestamp:日期.现在()});sendMessage({data:'helloworld',timestamp:Date.now()});这样就可以实现同源下两个标签页之间的通信。兼容性可以在caniuse官网上查询storageevent找到。IE的浏览器支持很不友好。Caniuse在某种程度上使用形容词“完全错误”来表达这一点。IE10的storage事件会在页面文档对象构建完成后触发,导致嵌套iframe页面出现很多问题;IE11的存储事件对象不区分oldValue和newValue,它们总是存储更新的值case3两个不相关的标签页之间的通信。这种情况是最迫切需要解决的问题。如何实现两个不相关的标签页之间的通信需要一定的技巧,需要同时修改这两个标签页的权限,否则无法实现这两个标签页的页面容量。当满足以上条件时,我们可以利用case1和case2的技术来满足case3的需求,这就需要我们巧妙的结合HTML5的postMessageAPI和存储事件来实现这两个不相关的标签页的连接。为此,我想到了iframe,通过在这两个tab页中嵌入同一个iframe页面来实现“桥接”,最终完成了通信:tabA----->iframeA[bridge.html]||\|/iframeB[bridge.html]----->tabB的单向通信原理如上图所示。选项卡A嵌入iframeA,选项卡B嵌入iframeB。这两个iframe引用同一个页面“bridge.html”。如果tabA向tabB发送消息,首先tabA通过postMessage消息发送给iframeA(tabA可以获得iframeA的window对象iframe.contentWindow);之后iframeA通过一条存储消息完成与iframeB的通信(因为iframeA和iframeB同源,所以这里可以使用case2的通信方式);最后,iframeB也使用postMessage向tabB发送消息(tabB的window对象在iframe中被window.parent引用)。至此,tabA的消息已经走完所有链接,成功到达tabB。反方向发送消息同理,这里不再赘述。接下来是talkischeap的链接,给我看看代码:tabA://发送消息到弹出的tab页window.sendMessageToTab=function(data){//由于[#J_bridge]的源文件iframe页面在vstudio服务器中,所以postMessage发送到“同源”document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify(data),'/');};//接收来自[#J_bridge]iframe的选项卡消息窗口parse(data));if(info.type=='BSays'){console.log('BSay:',info);}}catch(e){}});sendMessageToTab({type:'ASays',data:'helloworld,B'})bridge.htmlwindow.addEventListener("storage",function(ev){if(ev.key=='message'){window.parent.postMessage(ev.newValue,'*');}});函数message_broadcast(message){localStorage.setItem('message',JSON.stringify(message));localStorage.removeItem('message');}window.addEventListener('message',function(e){让{数据、来源、来源}=e;//收到父文档的消息后,广播到其他同源页面message_broadcast(data);});tabBwindow.addEventListener('message',function(e){let{data,source,origin}=e;if(!data)return;letinfo=JSON.parse(JSON.parse(data));if(info.type=='ASays'){document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({type:'BSays',data:'helloworldechofromB'}),'*');}});//tabB主动向tabAdocument发送消息.querySelector('button').addEventListener('click',function(){document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({type:'BSays',data:'IamB'}),'*');})至此,通过在tabA和tabB中引入具有“桥梁”功能的iframe[bridge.html]页面,实现了两个不相关的tab页之间的双向通信。此实现技术选项卡或窗口之间的强引用通信
