淺析瀏覽器跨頁面通信的方式:localStorage+StorageEvent事件、BroadCast Channel廣播通信、Service Worker消息中轉、postMessage、直接引用-window.open + window.opener、WebSocket服務端推送、利用iframe橋實現非同源頁面通信


  在瀏覽器中,我們可以同時打開多個Tab頁,每個Tab頁可以粗略理解為一個“獨立”的運行環境,即使是全局對象也不會在多個Tab間共享。然而有些時候,我們希望能在這些“獨立”的Tab頁面之間同步頁面的數據、信息或狀態。

  正如下面這個例子:我在列表頁點擊“收藏”后,對應的詳情頁按鈕會自動更新為“已收藏”狀態;類似的,在詳情頁點擊“收藏”后,列表頁中按鈕也會更新。這就是我們所說的前端跨頁面通信。那么你知道哪些跨頁面通信的方式呢?

  瀏覽器的同源策略在下述的一些跨頁面通信方法中依然存在限制。因此,我們先來看看,在滿足同源策略的情況下,都有哪些技術可以用來實現跨頁面通信。

一、同源頁面間的跨頁面通信1:localStorage

1、實現原理:一個窗口更新 localStorage,另一個窗口監聽 window 對象的 storage 事件來實現通信。

  注意:兩個頁面要同源(URL的協議、域名和端口相同)。要訪問一個localStorage對象,頁面必須來自同一個域名(子域名無效),使用同一種協議,在同一個端口上,相當於globalStorage[localhost.host]。

2、具體實現代碼:

// 一個窗口的設值代碼
localStorage.setItem('aaa', 10) // 其他窗口監聽storage事件
window.addEventListener("storage", function (e) { console.log(e) console.log(e.newValue) })

3、localStorage 可以實現同一瀏覽器多個標簽頁之間通信的原理

  localStorage是Storage對象的實例。對Storage對象進行任何修改,都會在文檔上觸發storage事件。當通過屬性或者setItem()方法保存數據,使用delete操作符或removeItem()刪除數據,或者調用clear()方法時,都會發生該事件。這個事件的event對象有以下屬性:

  • domain:發生變化的存儲空間的域名;
  • key:設置或者刪除的鍵名;
  • newValue:如果是設置值,則為新值;如果是刪除值,則是null;
  • oldValue:鍵被更改之前的值;

  注意:IE8和Firefox只實現了domin屬性。

4、只有值變化才會觸發 StorageEvent 事件

  注意這里有一個細節:我們在mydata上添加了一個取當前毫秒時間戳的.st屬性。這是因為,storage事件只有在值真正改變時才會觸發。舉個例子:

window.localStorage.setItem('test', '123'); window.localStorage.setItem('test', '123');

  由於第二次的值'123'與第一次的值相同,所以以上的代碼只會在第一次setItem時觸發storage事件。因此我們通過設置st來保證每次調用時一定會觸發storage事件。

mydata.st = +(new Date); window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));

二、同源頁面間的跨頁面通信2:BroadCast Channel

  BroadCast Channel 可以幫我們創建一個用於廣播的通信頻道。當所有頁面都監聽同一頻道的消息時,其中某一個頁面通過它發送的消息就會被其他所有頁面收到。它的API和用法都非常簡單。

// 下面的方式就可以創建一個標識為 A-Broad 的頻道:
const bc = new BroadcastChannel('A-Broad');

  各個頁面可以通過onmessage來監聽被廣播的消息:

bc.onmessage = function (e) { const data = e.data; const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[BroadcastChannel] receive message:', text); };

  要發送消息時只需要調用實例上的 postMessage 方法即可:

bc.postMessage(mydata);

三、同源頁面間的跨頁面通信3:Service Worker

  Service Worker 是一個可以長期運行在后台的 Worker,能夠實現與頁面的雙向通信。多頁面共享間的 Service Worker 可以共享,將 Service Worker 作為消息的處理中心(中央站)即可實現廣播效果。

1、注冊:首先,需要在頁面注冊 Service Worker

// 頁面邏輯
navigator.serviceWorker.register('../util.sw.js').then(function () { console.log('Service Worker 注冊成功'); });

  其中../util.sw.js是對應的 Service Worker 腳本。

2、消息中轉站邏輯

  Service Worker 本身並不自動具備“廣播通信”的功能,需要我們添加些代碼,將其改造成消息中轉站:

// ../util.sw.js Service Worker 邏輯
self.addEventListener('message', function (e) { console.log('service worker receive message', e.data); e.waitUntil( self.clients.matchAll().then(function (clients) { if (!clients || clients.length === 0) { return; } clients.forEach(function (client) { client.postMessage(e.data); }); }) ); });

  我們在 Service Worker 中監聽了message事件,獲取頁面(從 Service Worker 的角度叫 client)發送的信息。然后通過self.clients.matchAll()獲取當前注冊了該 Service Worker 的所有頁面,通過調用每個client(即頁面)的postMessage方法,向頁面發送消息。這樣就把從一處(某個Tab頁面)收到的消息通知給了其他頁面。

3、監聽消息

  處理完 Service Worker,我們需要在頁面監聽 Service Worker 發送來的消息:

/* 頁面邏輯 */ navigator.serviceWorker.addEventListener('message', function (e) { const data = e.data; const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[Service Worker] receive message:', text); });

4、發送消息:最后,當需要同步消息時,可以調用 Service Worker 的postMessage方法

/* 頁面邏輯 */ navigator.serviceWorker.controller.postMessage(mydata);

  上面我們看到了三種實現跨頁面通信的方式,不論是建立廣播頻道的 Broadcast Channel,還是使用 Service Worker 的消息中轉站,抑或是storage事件,其都是“廣播模式”:一個頁面將消息通知給一個“中央站”,再由“中央站”通知給各個頁面。

在上面的例子中,這個“中央站”可以是一個 BroadCast Channel 實例、一個 Service Worker 或是 LocalStorage。

四、同源頁面間的跨頁面通信5:postMessage

  這個就不用多說了,具體昨天總結的這篇博客:《淺析 postMessage 方法介紹、如何接收數據(監聽message事件及其屬性介紹)、使用postMessage的安全注意事項、具體使用方式(父子頁面如何互發消息、接收消息)

五、同源頁面間的跨頁面通信4:直接引用 - window.open + window.opener

  當我們使用window.open打開頁面時,方法會返回一個被打開頁面window的引用。而在未顯示指定noopener時,被打開的頁面可以通過window.opener獲取到打開它的頁面的引用 —— 通過這種方式我們就將這些頁面建立起了聯系(一種樹形結構)。

(一)消息發送方

1、首先,我們把window.open打開的頁面的window對象收集起來:

let childWins = []; document.getElementById('btn').addEventListener('click', function () { const win = window.open('./some/sample'); childWins.push(win); });

2、然后,當我們需要發送消息的時候,作為消息的發起方,一個頁面需要同時通知它打開的頁面與打開它的頁面:

// 過濾掉已經關閉的窗口
childWins = childWins.filter(w => !w.closed); if (childWins.length > 0) { mydata.fromOpenner = false; childWins.forEach(w => w.postMessage(mydata)); } if (window.opener && !window.opener.closed) { mydata.fromOpenner = true; window.opener.postMessage(mydata); }

  注意,我這里先用.closed屬性過濾掉已經被關閉的 Tab 窗口。這樣,作為消息發送方的任務就完成了。下面看看,作為消息接收方,它需要做什么。

(二)消息接收方

  此時,一個收到消息的頁面就不能那么自私了,除了展示收到的消息,它還需要將消息再傳遞給它所“知道的人”(打開與被它打開的頁面):

window.addEventListener('message', function (e) { const data = e.data; const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[Cross-document Messaging] receive message:', text); // 避免消息回傳
    if (window.opener && !window.opener.closed && data.fromOpenner) { window.opener.postMessage(data); } // 過濾掉已經關閉的窗口
    childWins = childWins.filter(w => !w.closed); // 避免消息回傳
    if (childWins && !data.fromOpenner) { childWins.forEach(w => w.postMessage(data)); } });

  這樣,每個節點(頁面)都肩負起了傳遞消息的責任,也就是所謂的“口口相傳”,而消息就在這個樹狀結構中流轉了起來

  顯然,“口口相傳”的模式存在一個問題:如果頁面不是通過在另一個頁面內的window.open打開的(例如直接在地址欄輸入,或從其他網站鏈接過來),這個聯系就被打破了。通過父子窗口的引用關系(以’window.open’或’target=_blank’方式打開子窗口),子窗口很容易感知到父窗口作用域值的變化,但是當子窗口刷新后,父子窗口之間的引用關系會消失,此時子窗口也不能接收到父窗口的消息。

  其實還有一種做法是通過 WebSocket 這類的“服務器推”技術來進行同步,這好比將我們的“中央站”從前端移到了后端。

六、WebSocket

1、實現原理:所有的 WebSocket 都監聽同一個服務器地址,利用send發送消息,利用onmessage獲取消息的變化。

2、優點:不僅能跨窗口,還能跨瀏覽器,兼容性最佳。

3、缺點:只是需要消耗點服務器資源。

七、非同源頁面之間的通信:iframe橋

  上面我們介紹了前端跨頁面通信的方法,但它們大都受到同源策略的限制。然而有時候,我們有兩個不同域名的產品線,也希望它們下面的所有頁面之間能無障礙地通信。那該怎么辦呢?

  要實現該功能,可以使用一個用戶不可見的 iframe 作為“橋”。由於 iframe 與父頁面間可以通過指定origin來忽略同源限制,因此可以在每個頁面中嵌入一個 iframe (例如:http://sample.com/bridge.html),而這些 iframe 由於使用的是一個 url,因此屬於同源頁面,其通信方式可以復用上面第一部分提到的各種方式。

1、頁面與 iframe 通信非常簡單,首先需要在頁面中監聽 iframe 發來的消息,做相應的業務處理:

/* 業務頁面代碼 */ window.addEventListener('message', function (e) { // …… do something
});

2、然后,當頁面要與其他的同源或非同源頁面通信時,會先給 iframe 發送消息

/* 業務頁面代碼 */ window.frames[0].window.postMessage(mydata, '*');

  其中為了簡便此處將postMessage的第二個參數設為了'*',你也可以設為 iframe 的 URL。

3、iframe 收到消息后,會使用某種跨頁面消息通信技術在所有 iframe 間同步消息,例如下面使用的 Broadcast Channel:

/* iframe 內代碼 */
const bc = new BroadcastChannel('A-Broad'); // 收到來自頁面的消息后,在 iframe 間進行廣播
window.addEventListener('message', function (e) { bc.postMessage(e.data); }); 

4、其他 iframe 收到通知后,則會將該消息同步給所屬的父頁面:

/* iframe 內代碼 */
// 對於收到的(iframe)廣播消息,通知給所屬的業務頁面
bc.onmessage = function (e) { window.parent.postMessage(e.data, '*'); };

  下圖就是使用 iframe 作為“橋”的非同源頁面間通信模式圖。

  其中“同源跨域通信方案”可以使用之前提到的某種技術。

八、總結:

1、對於同源頁面,常見的方式包括:

  • 廣播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
  • 共享存儲模式:Shared Worker / IndexedDB / cookie
  • 口口相傳模式:window.open + window.opener
  • 基於服務端:Websocket / Comet / SSE 等

2、而對於非同源頁面,則可以通過嵌入同源 iframe 作為“橋”,將非同源頁面通信轉換為同源頁面通信。

參考文章:https://juejin.cn/post/6844903811232825357


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM