六種瀏覽器跨窗口通信方案 - 從實際案例出發
瀏覽器跨窗口通信,聽上去挺有技術感的,但實現起來方案倒是挺多的。本文將從一個實際的業務開發場景,帶你了解如何實現瀏覽器跨窗口通信。
業務場景
一個常規的業務列表頁,頁面中提供了一個新增功能,由于新增功能的表單項內容比較多,所以交互上使用新開一個窗口來完成。這時問題來了,在新增完成后,如何通知列表頁面刷新列表數據,以便展示出剛才新增的那一條數據。
圖片
各位可以先自己在心中簡單想想,如果讓你實現這個功能,你會使用什么方案。
解決方案
window.opener
window.opener 代表的是打開當前窗口的那個窗口的引用,例如:在 window A 中打開了 window B,B.opener 返回 A。
有了這個引用關系,我們就可以實現跨窗口通信:
圖片
- 列表頁設置刷新列表方法 window.refreshList = () => {}
- 列表頁通過 window.open 或者 <a href="newUrl" target="_blank" rel="opener">新增</a> 打開新增功能頁面。
- 用戶完成新增表單填寫并提交,通過調用 window.opener.refreshList 方法來刷新列表頁面數據,并關閉當前頁。
有人可能注意到了,在 a 標簽中我們使用到了 rel="opener" 屬性,為什么要設置這個屬性呢?
rel 屬性定義了所鏈接的資源與當前文檔的關系,常見的屬性值有:
- opener: 打開的新頁面 window.opener 會指向前一個頁面的 window。
- noopener: 和 opener 相對應, window.opener 為空。
- noreferrer:打開新頁面時請求頭不會包含 Referer,比如你未設置 noreferrer 的情況下,從 antd 打開百度,百度頁面請求頭就有 Referer: https://ant.design/
- nofollow: 主要用于 SEO,告訴搜索引擎忽略該鏈接。
主要關注 opener 和 noopener 屬性,a 標簽默認情況下 rel=noopener,這代表打開的新增頁面的 window.opener 對象會為空,所以需要設置 rel=opener。
那么又有一個問題,為什么 a 標簽默認是 rel=noopener, 因為 opener 存在安全漏洞,比如你以 opener 的方式打開了一個未知的新頁面,這個新頁面可以通過 window.opener.location.href = 'fake.com' 重定向你的頁面。
而 window.open 默認情況下 rel=opener,故打開的新頁面可以拿到 window.opener 對象,不過要是打開第三方未知網站,建議設置為 noopener, 比如 window.open('https://baidu.com', 'title', 'noopener,noreferrer')。
BroadcastChannel
BroadcastChannel API 顧名思義,為“廣播頻道”,適用于在同一域名下的多個窗口、標簽頁或 iframe 之間進行實時消息廣播。它的使用也非常簡單,我們也看下如何通過它來實現上面的業務場景。
列表頁創建一個 BroadcastChannel 實例來監聽消息:
// 創建 BroadcastChannel 實例
const channel = new BroadcastChannel('myChannel');
// 監聽廣播通道的消息
channel.addEventListener('message', (event) => {
console.log('接收:', event.data); // { action: 'refresh' }
})
新增功能頁面同樣創建一個 BroadcastChannel 實例,頻道名稱需要和列表頁一致:
// 創建 BroadcastChannel 實例
const channel = new BroadcastChannel('myChannel');
// 向廣播通道發送消息
channel.postMessage({ action: 'refresh' });
// 關閉頻道
channel.close()
可以看到 BroadcastChannel 的使用很簡單,雙方創建同名頻道的 BroadcastChannel 實例,然后一方監聽 message 事件,一方使用 postMessage 廣播內容。
postMessage
對于 postMessage,我們最常用的方式應該就是當前頁面和 iframe 的跨域消息通信了,其實它也能完成跨窗口通信,核心就是能拿到新窗口的 window 對象,這個在 window.opener 方案中我們就知道通過設置 rel="opener" 就可以辦到。
列表頁打開新窗口,并監聽 message 事件:
<a href="./add.html" target="_blank" rel="opener">新增</a>
<script>
// 不同與 BroadcastChannel,這邊是監聽 window 上的 message 事件
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
console.log('接收:', event.data); // { action: 'refresh' }
}
</script>
新增功能頁面使用 window.opener.postMessage 發送消息:
window.opener?.postMessage({ action: 'refresh' }, '*');
至此我們已經完成了上面的業務需求,postMessage 的優勢在于可以跨域。
MessageChannel
MessageChannel API 允許我們創建一個新的消息通道,并通過它的兩個 MessagePort 實例屬性發送數據,同時它也可以跨域通信。
不同于 BroadcastChannel 的廣播,MessageChannel 只提供雙向通信通道,不過它既可以像 postMessage 一樣用于 iframe 通信,也可以用于 Web Worker 之間進行通信。
圖片
要用 MessageChannel 實現跨窗口通信,方式有點類似 postMessage, 打開新頁面時需要設置 rel="opener"。
列表頁初始化 MessageChannel 實例,并在 port1 上監聽 message 事件:
// 創建 MessageChannel 實例
window.messageChannel = new MessageChannel();
const port1 = window.messageChannel.port1;
// port1 監聽 message 事件
port1.onmessage = function(event) {
console.log('接收:', event.data); // { action: 'refresh' }
};
新增功能頁面使用 window.opener.messageChannel 拿到列表的 MessageChannel 實例,并使用 port2 的 postMessage 方法往 port1 通道上發送消息:
// 獲取 MessageChannel 實例
const messageChannel = window.opener.messageChannel;
const port2 = messageChannel.port2;
// 往 port1 發送消息
port2.postMessage({ action: 'refresh' });
需要注意的是 MessagePort 對象如果使用 addEventListener 監聽 message 事件,就需要調用下 port.start() 方法,使用 onmessage 則可以不需要。
storage 事件
當 localStorage 或 sessionStorage 被修改時,將觸發 storage 事件,利用這個機制,我們也可以完成跨窗口通信。同時因為用的是 localStorage 或 sessionStorage 方式,所以頁面必須是同一域名下。
值得注意的是,sessionStorage 并不能滿足上面的業務需求,sessionStorage 要想觸發 storage 事件,必須在同一窗口,也就是一般只在當前頁和其加載的同域名 iframe 下使用。還有一點就是當前頁的 setItem 不會觸發當前頁的 storage 事件,只會觸發其他窗口的。
列表頁監聽 storage 事件,判斷是否是對應 key 值發生變化:
window.addEventListener("storage", () => {
console.log('發生變化的值:', event.key);
if (event.key === 'refresh') {
// 刷新列表
}
});
新增功能頁面使用 localStorage 的 setItem 來觸發列表的 storage 事件:
window.localStorage.setItem('fresh', Date.now())
SharedWorker
SharedWorker 是 Web Workers API 的一種擴展,它允許在多個瀏覽器上下文中(例如多個頁面或多個 iframe )共享一個 Worker。ShareWorker 遵守同源策略,也就是必須在同一域名下使用 SharedWorker。
先寫個 worker.js 腳本:
const ports = [];
onconnect = function (e) {
const port = e.ports[0];
ports.push(port);
port.onmessage = function (e) {
console.log("worker接收到的消息:", e.data);
ports.forEach((p) => {
p.postMessage(e.data);
});
};
};
列表頁創建 ShareWorker 實例,然后監聽 message 事件:
const sharedWorker = new SharedWorker('./worker.js', 'test');
const port = sharedWorker.port;
port.onmessage = function (event) {
console.log('接收:', event.data); // { action: 'refresh' }
};
新增功能頁面使用 postMessage 發送消息給 worker,worker 在發送給各個主線程:
const sharedWorker = new SharedWorker('./worker.js', 'test');
const port = sharedWorker.port;
port.postMessage({ action: 'refresh' });
這樣我們就完成了上述的業務需求。
總結
上述的各種方式都可以實現通知列表頁去做刷新動作,不過更推薦使用 window.opener 或 BroadcastChannel 來實現,這兩種方式相對使用簡單并且很符合這個業務場景。
對于其他需要跨窗口通信的場景,可以根據各個 API 的能力特點來選擇使用哪個。