淺析瀏覽器跨頁面通訊的方式: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