當前頁面須要與當前瀏覽器已打開的的某個tab頁通訊,完成某些交互。其中,與當前頁面待通訊的tab頁能夠是與當前頁面同域(相同的協議、域名和端口),也能夠是跨域的。html
要實現這個特殊的功能,單單使用HTML5的相關特性是沒法完成的,須要有更加巧妙的設計。windows
如今咱們發現下思惟,假設多種場景下的解決方案,最終尋找通用解。跨域
兩個須要交互的tab頁面具備依賴關係。瀏覽器
如 A頁面中經過JavaScript的window.open打開B頁面,或者B頁面經過iframe嵌入至A頁面
,此種情形最簡單,能夠經過HTML5的 window.postMessage
API完成通訊,因爲postMessage函數是綁定在 window 全局對象下,所以通訊的頁面中必須有一個頁面(如A頁面)能夠獲取另外一個頁面(如B頁面)的window對象,這樣才能夠完成單向通訊;B頁面無需獲取A頁面的window對象,若是須要B頁面對A頁面的通訊,只須要在B頁面偵聽message事件,獲取事件中傳遞的source對象,該對象即爲A頁面window對象的引用:服務器
B頁面 window.addEventListner('message',(e)=>{ let {data,source,origin} = e; source.postMessage('message echo','/'); });
postMessage的第一個參數爲消息實體,它是一個結構化對象,便可以經過「JSON.stringify和JSON.parse」函數還原的對象;第二個參數爲消息發送範圍選擇器,設置爲「/」意味着只發送消息給同源的頁面,設置爲「*」則發送所有頁面。函數
兩個打開的頁面屬於同源範疇。post
若要實現兩個互不相關的通源tab頁面通訊,可使用一種比較巧妙的方式:localstorage。localStorage的存儲遵循同源策略,所以同源的兩個tab頁面能夠經過這種共享localStorage的方式實現通訊,經過約定localStorage的某一個itemName,基於該key值的內容做爲「共享硬盤」方式通訊。性能
不過,若是單純使用localStorage存儲作通訊方式會遇到一個問題,就是兩個頁面把握不許通訊時機,若是A頁面此刻須要發送給B頁面一條消息「hello B」,它會設置localStorage.setItem('message','hello B'),而且採用setTimeout輪訓等待B的消息;而B此刻也一樣使用setTimeout輪訓等待localStorage的message項的變化,當獲取到'message'字段時,便取出消息'hello B'。B若是要發消息給A,仍然採用一樣套路。網站
這種方式性能極其低下,須要通訊兩方不停的監聽localStorage某項的變化,及其浪費事件隊列處理效率。幸虧,HTML5提供了storage事件,經過window對象偵聽storage事件,會偵聽localStorage對象的變化事件(包括item的添加、修改和刪除)。所以,經過事件能夠完成高效的通訊機制:this
A 頁面 window.addEventListener("storage", function(ev){ if (ev.key == 'message') { // removeItem一樣觸發storage事件,此時ev.newValue爲空 if(!ev.newValue) return; var message = JSON.parse(ev.newValue); console.log(message); } }); function sendMessage(message){ localStorage.setItem('message',JSON.stringify(message)); localStorage.removeItem('message'); } // 發送消息給B頁面 sendMessage('this is message from A');
B 頁面 window.addEventListener("storage", function(ev){ if (ev.key == 'message') { // removeItem一樣觸發storage事件,此時ev.newValue爲空 if(!ev.newValue) return; var message = JSON.parse(ev.newValue); // 發送消息給A頁面 sendMessage('message echo from B'); } }); function sendMessage(message){ localStorage.setItem('message',JSON.stringify(message)); localStorage.removeItem('message'); }
發送消息採用sendMessage函數,該函數序列化消息,設置爲localStorage的message字段值後,刪除該message字段。這樣作的目的是不污染localStorage空間,可是會形成一個無傷大雅的副作用,即觸發兩次storage事件,所以咱們在storage事件處理函數中作了if(!ev.newValue) return;
判斷。
當咱們在A頁面中執行sendMessage函數,其餘同源頁面會觸發storage事件,而A頁面卻不會觸發storage事件;並且連續發送兩次相同的消息也只會觸發一次storage事件,若是須要解決這種狀況,能夠在消息體體內加入時間戳:
sendMessage({ data: 'hello world', timestamp: Date.now() }); sendMessage({ data: 'hello world', timestamp: Date.now() });
經過這種方式,能夠實現同源下的兩個tab頁通訊,兼容性
經過caniuse網站查詢storage事件發現,IE的瀏覽器支持很是的不友好,caniuse使用了「completely wrong」的形容詞來表述這一程度。IE10的storage事件會在頁面document文檔對象構建完成後觸發,這在嵌套iframe的頁面中形成諸多問題;IE11的storage Event對象卻不區分oldValue和newValue值,它們始終存儲更新後的值
兩個互不相關的tab頁面通訊。
這種狀況纔是最急需解決的問題,如何實現兩個沒有任何關係的tab頁面通訊,這須要一些技巧,並且須要有同時修改這兩個tab頁面的權限,不然根本不可能實現這兩個tab頁的能力。
在上述條件知足的狀況下,咱們就可使用case1 和 case2的技術完成case 3的需求,這須要咱們巧妙的結合HTML5 postMessage API 和 storage事件實現這兩個毫無關係的tab頁面的連通。爲此,我想到了iframe,經過在這兩個tab頁嵌入同一個iframe頁實現「橋接」,最終完成通訊:
tab A -----> iframe A[bridge.html] | | \|/ iframe B[bridge.html] -----> tab B
單方向的通訊原理如上圖所示,tab A中嵌入iframe A,tab B中嵌入iframe B,這兩個iframe引用相同的頁面「bridge.html」。若是tab A發消息給tab B,首先tab A經過postMessage消息發送給iframe A(tab A能夠獲取到iframe A的window對象iframe.contentWindow);此後iframe A經過storage消息完成與iframe B的通訊(因爲iframeA 與iframe B同源,所以case 2的通訊方式這裏可使用);最終,iframe B一樣採用postMessage方式發送消息給tab B(在iframe中經過window.parent引用tab B的window對象)。至此,tab A的消息走通了全部鏈路,成功抵達tab B。
反方向發送消息一樣的道理,這裏就不在詳細說明。接下來到了 talk is cheap,show me the code 環節:
tab A: // 向彈出的tab頁面發送消息 window.sendMessageToTab = function(data){ // 因爲[#J_bridge]iframe頁面的源文件在vstudio服務器中,所以postMessage發向「同源」 document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify(data),'/'); }; // 接收來自 [#J_bridge]iframe的tab消息 window.addEventListener('message',function(e){ let {data,source,origin} = e; if(!data) return; try{ let info = JSON.parse(JSON.parse(data)); if(info.type == 'BSays'){ console.log('BSay:',info); } }catch(e){ } }); sendMessageToTab({ type: 'ASays', data: 'hello world, B' })
bridge.html window.addEventListener("storage", function(ev){ if (ev.key == 'message') { window.parent.postMessage(ev.newValue,'*'); } }); function message_broadcast(message){ localStorage.setItem('message',JSON.stringify(message)); localStorage.removeItem('message'); } window.addEventListener('message',function(e){ let {data,source,origin} = e; // 接受到父文檔的消息後,廣播給其餘的同源頁面 message_broadcast(data); });
tab B window.addEventListener('message',function(e){ let {data,source,origin} = e; if(!data) return; let info = JSON.parse(JSON.parse(data)); if(info.type == 'ASays'){ document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({ type: 'BSays', data: 'hello world echo from B' }),'*'); } }); // tab B主動發送消息給tab A document.querySelector('button').addEventListener('click',function(){ document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({ type: 'BSays', data: 'I am B' }),'*'); })
至此,經過在tab A和tab B中引入「橋接」功能的iframe[bridge.html]頁面,實現了兩個無關tab頁的雙向通訊,這種實現的技巧性較強。