面試官:前端跨頁面通訊,你知道哪些方法?
在瀏覽器中,我們可以同時開啟多個Tab頁,每個Tab頁可以粗略理解為一個“獨立”的執行環境,即使是全域性物件也不會在多個Tab間共享。然而有些時候,我們希望能在這些“獨立”的Tab頁面之間同步頁面的資料、資訊或狀態。
正如下面這個例子:我在列表頁點選“收藏”後,對應的詳情頁按鈕會自動更新為“已收藏”狀態;類似的,在詳情頁點選“收藏”後,列表頁中按鈕也會更新。

這就是我們所說的前端跨頁面通訊。
你知道哪些跨頁面通訊的方式呢?如果不清楚,下面我就帶大家來看看七種跨頁面通訊的方式。
一、同源頁面間的跨頁面通訊
以下各種方式的 線上 Demo 可以戳這裡 >>
瀏覽器的同源策略在下述的一些跨頁面通訊方法中依然存在限制。因此,我們先來看看,在滿足同源策略的情況下,都有哪些技術可以用來實現跨頁面通訊。
1. BroadCast Channel
BroadCast Channel 可以幫我們建立一個用於廣播的通訊頻道。當所有頁面都監聽同一頻道的訊息時,其中某一個頁面通過它傳送的訊息就會被其他所有頁面收到。它的API和用法都非常簡單。
下面的方式就可以建立一個標識為 AlienZHOU
的頻道:
const bc = new BroadcastChannel('AlienZHOU'); 複製程式碼
各個頁面可以通過 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); 複製程式碼
Broadcast Channel 的具體的使用方式可以看這篇 《【3分鐘速覽】前端廣播式通訊:Broadcast Channel》 。
2. Service Worker
Service Worker 是一個可以長期執行在後臺的 Worker,能夠實現與頁面的雙向通訊。多頁面共享間的 Service Worker 可以共享,將 Service Worker 作為訊息的處理中心(中央站)即可實現廣播效果。
Service Worker 也是 PWA 中的核心技術之一,由於本文重點不在 PWA ,因此如果想進一步瞭解 Service Worker,可以閱讀我之前的文章 【PWA學習與實踐】(3) 讓你的WebApp離線可用 。
首先,需要在頁面註冊 Service Worker:
/* 頁面邏輯 */ navigator.serviceWorker.register('../util.sw.js').then(function () { console.log('Service Worker 註冊成功'); }); 複製程式碼
其中 ../util.sw.js
是對應的 Service Worker 指令碼。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頁面)收到的訊息通知給了其他頁面。
處理完 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); }); 複製程式碼
最後,當需要同步訊息時,可以呼叫 Service Worker 的 postMessage
方法:
/* 頁面邏輯 */ navigator.serviceWorker.controller.postMessage(mydata); 複製程式碼
3. LocalStorage
LocalStorage 作為前端最常用的本地儲存,大家應該已經非常熟悉了;但 StorageEvent
這個與它相關的事件有些同學可能會比較陌生。
當 LocalStorage 變化時,會觸發 storage
事件。利用這個特性,我們可以在傳送訊息時,把訊息寫入到某個 LocalStorage 中;然後在各個頁面內,通過監聽 storage
事件即可收到通知。
window.addEventListener('storage', function (e) { if (e.key === 'ctc-msg') { const data = JSON.parse(e.newValue); const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[Storage I] receive message:', text); } }); 複製程式碼
在各個頁面新增如上的程式碼,即可監聽到 LocalStorage 的變化。當某個頁面需要傳送訊息時,只需要使用我們熟悉的 setItem
方法即可:
mydata.st = +(new Date); window.localStorage.setItem('ctc-msg', JSON.stringify(mydata)); 複製程式碼
注意,這裡有一個細節:我們在mydata上添加了一個取當前毫秒時間戳的 .st
屬性。這是因為, storage
事件只有在值真正改變時才會觸發。舉個例子:
window.localStorage.setItem('test', '123'); window.localStorage.setItem('test', '123'); 複製程式碼
由於第二次的值 '123'
與第一次的值相同,所以以上的程式碼只會在第一次 setItem
時觸發 storage
事件。因此我們通過設定 st
來保證每次呼叫時一定會觸發 storage
事件。
小憩一下
上面我們看到了三種實現跨頁面通訊的方式,不論是建立廣播頻道的 Broadcast Channel,還是使用 Service Worker 的訊息中轉站,抑或是些 tricky 的 storage
事件,其都是“廣播模式”:一個頁面將訊息通知給一個“中央站”,再由“中央站”通知給各個頁面。
在上面的例子中,這個“中央站”可以是一個 BroadCast Channel 例項、一個 Service Worker 或是 LocalStorage。
下面我們會看到另外兩種跨頁面通訊方式,我把它稱為“共享儲存+輪詢模式”。
4. Shared Worker
Shared Worker 是 Worker 家族的另一個成員。普通的 Worker 之間是獨立執行、資料互不相通;而多個 Tab 註冊的 Shared Worker 則可以實現資料共享。
Shared Worker 在實現跨頁面通訊時的問題在於,它無法主動通知所有頁面,因此,我們會使用輪詢的方式,來拉取最新的資料。思路如下:
讓 Shared Worker 支援兩種訊息。一種是 post,Shared Worker 收到後會將該資料儲存下來;另一種是 get,Shared Worker 收到該訊息後會將儲存的資料通過 postMessage
傳給註冊它的頁面。也就是讓頁面通過 get 來主動獲取(同步)最新訊息。具體實現如下:
首先,我們會在頁面中啟動一個 Shared Worker,啟動方式非常簡單:
// 建構函式的第二個引數是 Shared Worker 名稱,也可以留空 const sharedWorker = new SharedWorker('../util.shared.js', 'ctc'); 複製程式碼
然後,在該 Shared Worker 中支援 get 與 post 形式的訊息:
/* ../util.shared.js: Shared Worker 程式碼 */ let data = null; self.addEventListener('connect', function (e) { const port = e.ports[0]; port.addEventListener('message', function (event) { // get 指令則返回儲存的訊息資料 if (event.data.get) { data && port.postMessage(data); } // 非 get 指令則儲存該訊息資料 else { data = event.data; } }); port.start(); }); 複製程式碼
之後,頁面定時傳送 get 指令的訊息給 Shared Worker,輪詢最新的訊息資料,並在頁面監聽返回資訊:
// 定時輪詢,傳送 get 指令的訊息 setInterval(function () { sharedWorker.port.postMessage({get: true}); }, 1000); // 監聽 get 訊息的返回資料 sharedWorker.port.addEventListener('message', (e) => { const data = e.data; const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[Shared Worker] receive message:', text); }, false); sharedWorker.port.start(); 複製程式碼
最後,當要跨頁面通訊時,只需給 Shared Worker postMessage
即可:
sharedWorker.port.postMessage(mydata); 複製程式碼
注意,如果使用 addEventListener
來新增 Shared Worker 的訊息監聽,需要顯式呼叫 MessagePort.start
方法,即上文中的 sharedWorker.port.start()
;如果使用 onmessage
繫結監聽則不需要。
5. IndexedDB
除了可以利用 Shared Worker 來共享儲存資料,還可以使用其他一些“全域性性”(支援跨頁面)的儲存方案。例如IndexedDB 或 cookie。
鑑於大家對 cookie 已經很熟悉,加之作為“網際網路最早期的儲存方案之一”,cookie 已經在實際應用中承受了遠多於其設計之初的責任,我們下面會使用 IndexedDB 來實現。
其思路很簡單:與 Shared Worker 方案類似,訊息傳送方將訊息存至 IndexedDB 中;接收方(例如所有頁面)則通過輪詢去獲取最新的資訊。在這之前,我們先簡單封裝幾個 IndexedDB 的工具方法。
- 開啟資料庫連線:
function openStore() { const storeName = 'ctc_aleinzhou'; return new Promise(function (resolve, reject) { if (!('indexedDB' in window)) { return reject('don\'t support indexedDB'); } const request = indexedDB.open('CTC_DB', 1); request.onerror = reject; request.onsuccess =e => resolve(e.target.result); request.onupgradeneeded = function (e) { const db = e.srcElement.result; if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) { const store = db.createObjectStore(storeName, {keyPath: 'tag'}); store.createIndex(storeName + 'Index', 'tag', {unique: false}); } } }); } 複製程式碼
- 儲存資料
function saveData(db, data) { return new Promise(function (resolve, reject) { const STORE_NAME = 'ctc_aleinzhou'; const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const request = store.put({tag: 'ctc_data', data}); request.onsuccess = () => resolve(db); request.onerror = reject; }); } 複製程式碼
- 查詢/讀取資料
function query(db) { const STORE_NAME = 'ctc_aleinzhou'; return new Promise(function (resolve, reject) { try { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const dbRequest = store.get('ctc_data'); dbRequest.onsuccess = e => resolve(e.target.result); dbRequest.onerror = reject; } catch (err) { reject(err); } }); } 複製程式碼
剩下的工作就非常簡單了。首先開啟資料連線,並初始化資料:
openStore().then(db => saveData(db, null)) 複製程式碼
對於訊息讀取,可以在連線與初始化後輪詢:
openStore().then(db => saveData(db, null)).then(function (db) { setInterval(function () { query(db).then(function (res) { if (!res || !res.data) { return; } const data = res.data; const text = '[receive] ' + data.msg + ' —— tab ' + data.from; console.log('[Storage I] receive message:', text); }); }, 1000); }); 複製程式碼
最後,要傳送訊息時,只需向 IndexedDB 儲存資料即可:
openStore().then(db => saveData(db, null)).then(function (db) { // …… 省略上面的輪詢程式碼 // 觸發 saveData 的方法可以放在使用者操作的事件監聽內 saveData(db, mydata); }); 複製程式碼
小憩一下
在“廣播模式”外,我們又瞭解了“共享儲存+長輪詢”這種模式。也許你會認為長輪詢沒有監聽模式優雅,但實際上,有些時候使用“共享儲存”的形式時,不一定要搭配長輪詢。
例如,在多 Tab 場景下,我們可能會離開 Tab A 到另一個 Tab B 中操作;過了一會我們從 Tab B 切換回 Tab A 時,希望將之前在 Tab B 中的操作的資訊同步回來。這時候,其實只用在 Tab A 中監聽 visibilitychange
這樣的事件,來做一次資訊同步即可。
下面,我會再介紹一種通訊方式,我把它稱為“口口相傳”模式。
6. window.open + window.opener
當我們使用 window.open
開啟頁面時,方法會返回一個被開啟頁面 window
的引用。而在未顯示指定 noopener
時,被開啟的頁面可以通過 window.opener
獲取到開啟它的頁面的引用 —— 通過這種方式我們就將這些頁面建立起了聯絡(一種樹形結構)。
首先,我們把 window.open
開啟的頁面的 window
物件收集起來:
let childWins = []; document.getElementById('btn').addEventListener('click', function () { const win = window.open('./some/sample'); childWins.push(win); }); 複製程式碼
然後,當我們需要傳送訊息的時候,作為訊息的發起方,一個頁面需要同時通知它開啟的頁面與開啟它的頁面:
// 過濾掉已經關閉的視窗 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
開啟的(例如直接在位址列輸入,或從其他網站連結過來),這個聯絡就被打破了。
除了上面這六個常見方法,其實還有一種(第七種)做法是通過 WebSocket 這類的“伺服器推”技術來進行同步。這好比將我們的“中央站”從前端移到了後端。
關於 WebSocket 與其他“伺服器推”技術,不瞭解的同學可以閱讀這篇 《各類“伺服器推”技術原理與例項(Polling/COMET/SSE/WebSocket)》
此外,我還針對以上各種方式寫了一個線上演示的 Demo >>

二、非同源頁面之間的通訊
上面我們介紹了七種前端跨頁面通訊的方法,但它們大都受到同源策略的限制。然而有時候,我們有兩個不同域名的產品線,也希望它們下面的所有頁面之間能無障礙地通訊。那該怎麼辦呢?
要實現該功能,可以使用一個使用者不可見的 iframe 作為“橋”。由於 iframe 與父頁面間可以通過指定 origin
來忽略同源限制,因此可以在每個頁面中嵌入一個 iframe (例如: http://sample.com/bridge.html
),而這些 iframe 由於使用的是一個 url,因此屬於同源頁面,其通訊方式可以複用上面第一部分提到的各種方式。
頁面與 iframe 通訊非常簡單,首先需要在頁面中監聽 iframe 發來的訊息,做相應的業務處理:
/* 業務頁面程式碼 */ window.addEventListener('message', function (e) { // …… do something }); 複製程式碼
然後,當頁面要與其他的同源或非同源頁面通訊時,會先給 iframe 傳送訊息:
/* 業務頁面程式碼 */ window.frames[0].window.postMessage(mydata, '*'); 複製程式碼
其中為了簡便此處將 postMessage
的第二個引數設為了 '*'
,你也可以設為 iframe 的 URL。iframe 收到訊息後,會使用某種跨頁面訊息通訊技術在所有 iframe 間同步訊息,例如下面使用的 Broadcast Channel:
/* iframe 內程式碼 */ const bc = new BroadcastChannel('AlienZHOU'); // 收到來自頁面的訊息後,在 iframe 間進行廣播 window.addEventListener('message', function (e) { bc.postMessage(e.data); }); 複製程式碼
其他 iframe 收到通知後,則會將該訊息同步給所屬的頁面:
/* iframe 內程式碼 */ // 對於收到的(iframe)廣播訊息,通知給所屬的業務頁面 bc.onmessage = function (e) { window.parent.postMessage(e.data, '*'); }; 複製程式碼
下圖就是使用 iframe 作為“橋”的非同源頁面間通訊模式圖。

其中“同源跨域通訊方案”可以使用文章第一部分提到的某種技術。
總結
今天和大家分享了一下跨頁面通訊的各種方式。
對於同源頁面,常見的方式包括:
- 廣播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
- 共享儲存模式:Shared Worker / IndexedDB / cookie
- 口口相傳模式:window.open + window.opener
- 基於服務端:Websocket / Comet / SSE 等
而對於非同源頁面,則可以通過嵌入同源 iframe 作為“橋”,將非同源頁面通訊轉換為同源頁面通訊。
本文在分享的同時,也是為了拋轉引玉。如果你有什麼其他想法,歡迎一起討論,提出你的見解和想法~