應(yīng)用場(chǎng)景:
- 狀態(tài)同步:多標(biāo)簽頁(yè)之間同步數(shù)據(jù),比如同步設(shè)備展示狀態(tài),同步數(shù)據(jù)信息
- 消息通知:通知其余標(biāo)簽頁(yè)執(zhí)行動(dòng)作,比如說(shuō)跳轉(zhuǎn)其他頁(yè)面,完成后,通知打開(kāi)頁(yè)面執(zhí)行狀態(tài)變更或刷新燈操作
- 隱私數(shù)據(jù)通信:基于加殼客戶端,實(shí)現(xiàn) H5 頁(yè)面之間的本地通信機(jī)制,不經(jīng)過(guò)網(wǎng)絡(luò)層傳輸,還根據(jù)隨機(jī)加密值實(shí)現(xiàn)跨權(quán)限式加密信道
實(shí)現(xiàn)場(chǎng)景:
- 需要跨域:Websocket、父子窗口
- 無(wú)需跨域:LocalStorage、SharedWorker、BroadcastChannel
LocalStroage
利用同域下 LocalStorage 共享策略
- 實(shí)現(xiàn)跨標(biāo)簽頁(yè)的信息同步
- 實(shí)現(xiàn)簡(jiǎn)單
存在問(wèn)題:
- LocalStorage 有 5MB 大小限制,需要注意消息限制問(wèn)題
使用:
- A 頁(yè)面監(jiān)聽(tīng)
storage事件 - B 頁(yè)面通過(guò)調(diào)用 LocalStorage 方法,觸發(fā)事件,傳給 A 頁(yè)面進(jìn)行使用
- 通過(guò)獲取
e.newValue變化即可得知當(dāng)前的傳送的消息 - 當(dāng)執(zhí)行
removeItem時(shí),e.newValue會(huì)變?yōu)?null - 通過(guò)
e.url判斷消息是否為自己發(fā)出,避免重復(fù)處理,或者監(jiān)聽(tīng)消息來(lái)源
- 通過(guò)獲取
// A 頁(yè)面
window.addEventListener('storage',(e)=>{
console.log(e.key) // key
console.log(e.oldValue) // 舊值
console.log(e.newValue) // 新值,即設(shè)置的值
console.log(e.storageArea) // 被操作的 storage 對(duì)象
console.log(e.url) // 文檔改變的對(duì)象地址來(lái)源
})
// B 頁(yè)面
window.LocalStorage.setItem('xx','xxx')
window.LocalStorage.removeItem('xxx','xxx')
SharedWorker
基于共享線程來(lái)完成通信,是獨(dú)立于主線程的后臺(tái)共享線程
SharedWorker 本身承載業(yè)務(wù)共享邏輯,底層通信手段基于 MessagePort 實(shí)現(xiàn)
- 通過(guò)
port通信降低主線程和 Shared Worker 的耦合度 - 生命周期和連接的主線程相關(guān),主線程全部釋放,SharedWorker 也會(huì)終止(可能會(huì)等待異步任務(wù)執(zhí)行完成,但是每測(cè)試出來(lái))
- 可能異常情況:
-
SecurityError:不能正常啟動(dòng) Shraed Worker -
NetworkError:Shared Worker 不是application/json格式 -
SyntaxError:URL 無(wú)法解析
-
存在問(wèn)題:
- 兼容性問(wèn)題,很多移動(dòng)端瀏覽器不支持
- 增加請(qǐng)求和維護(hù)成本,定義額外的 js 文件
- 調(diào)試?yán)щy,需要通過(guò)
chrome://inspect#workers界面查看調(diào)試信息
shared worker 調(diào)試界面,點(diǎn)擊 inspect 查看調(diào)試信息
使用:
主線程:
- 連接 SharedWorker
- 通過(guò)
shareWorker.port.postMessage向所有連接的頁(yè)面發(fā)送消息 - 通過(guò)
shareWorker.port.onmessage接收發(fā)來(lái)的消息
// 頁(yè)面內(nèi)(主線程)
const shareWorker = new ShareWorker('share-worker.js')
shareWorker.port.onmessage = (e) => {
const { type, data } = e.data
if(type === 'BROADCAST'){
// 處理邏輯
}
}
// 處理連接錯(cuò)誤
worker.port.onerror = function (error) {
console.error("Shared Worker 錯(cuò)誤:", error);
};
// 顯示激活
shareWorker.port.start()
function sendMessage(){
shareWorker.port.postMessage({type:'MESSAGE',data:{...}})
}
function disconnect(){
shareWorker.port.postMessage({type:'DISCONNECT'})
shareWorker.port.close()
}
shared worker文件:
- 通過(guò)
self.onconnect監(jiān)聽(tīng)連接事件,只要完成初始化就會(huì)觸發(fā) - 使用 Set 集合
connections存儲(chǔ)端口,支持 O(1) 操作消耗 - 操作:
- 通過(guò)
port.onmessage監(jiān)聽(tīng)任一主線程發(fā)來(lái)的消息,遍歷connections廣播消息。 - 在使用完成時(shí),通過(guò)
port.close關(guān)閉端口并清除端口在connections中的緩存,一定要清除連接,避免出現(xiàn)緩存端口導(dǎo)致廣播無(wú)效端口的情況出現(xiàn)。 -
port.start用于觸發(fā)端口激活,調(diào)用不會(huì)觸發(fā)什么內(nèi)容,但是部分瀏覽器不調(diào)用可能會(huì)阻塞消息發(fā)送。
- 通過(guò)
// share-worker.js (share Worker線程)
// 通過(guò) connections 管理端口
const connections = new Set();
// 消息廣播
function broadcast(){
connections.forEach(p => {
// 排除自身
if (p !== port) {
p.postMessage({ type: 'BROADCAST', data: `${data}` });
}
});
}
function disconnect(port){
connections.delete(port);
port.close(); // 關(guān)閉端口
}
self.onconnect = (e) => {
// 1. 讀取端口:自動(dòng)去重
const port = e.ports[0];
connections.add(port);
// 2. 監(jiān)聽(tīng)消息
port.onmessage = (e) => {
const { type, data } = e.data;
switch (type) {
case 'MESSAGE':
console.log('收到主線程消息:', data);
// 廣播給所有連接的主線程
broadcast(data,port)
break;
case 'DISCONNECT':
disconnect(port)
break;
}
};
// 3. 監(jiān)聽(tīng)端口錯(cuò)誤/關(guān)閉,自動(dòng)清理無(wú)效端口
port.onerror = (err) => {
console.log('端口錯(cuò)誤:', err);
connections.delete(port);
};
// 顯示激活端口(部分瀏覽器需顯式調(diào)用)
port.start();
};
Brocastchannel
支持同源下跨標(biāo)簽頁(yè)通信方案,適用于廣播消息方案
- 創(chuàng)建頻道名稱,只要在同個(gè)頻道名稱下的標(biāo)簽,都能接收廣播消息
- 無(wú)需像 SharedWorker 那樣遍歷廣播,直接發(fā)送即可
- 實(shí)現(xiàn)簡(jiǎn)單,幾行代碼即可搞定
存在問(wèn)題:
- 2022 年才穩(wěn)定的 API ,需要考慮下兼容性問(wèn)題
- 無(wú)法控制廣播域,需要通過(guò)多頻道來(lái)區(qū)分廣播形式,或者通過(guò)標(biāo)識(shí)符判斷要處理消息的頁(yè)面
使用:
- 通過(guò)
new BroadcastChannel(channelName)創(chuàng)建頻道 - 通過(guò)監(jiān)聽(tīng)
message來(lái)監(jiān)聽(tīng)廣播消息 - 通過(guò)
channel.postMessage()發(fā)送消息
const broadcastChannel = new BroadcastChannel("channel");
// 接收其他同頻道的廣播消息
broadcastChannel.addEventListener("message", (e) => {
const { type,data } = e.data
});
// 為同頻道廣播消息
broadcastChannle.postMessage({type:'message',data})
父子窗口
當(dāng)使用 window.open() 打開(kāi)窗口,可以通過(guò)父子窗口執(zhí)行通信
- 利用 瀏覽器窗口句柄(Window Handle)和消息傳遞 支持了父子通信
- 可以使用父窗口作為中間層,實(shí)現(xiàn)各個(gè)子窗口的流通性以及廣播功能
- 兼容性好
存在問(wèn)題:
- 需要窗口之間存在父子引用關(guān)系
- 需要額外維護(hù)子窗口的引用信息
使用:
- 父窗口通過(guò)
window.open打開(kāi)子窗口,同時(shí)保留子窗口的引用 - 基于
window.name區(qū)分子窗口,便于后續(xù)跨子窗口通信 - 監(jiān)聽(tīng)各自
window的message事件實(shí)現(xiàn)通信 - 父窗口通過(guò)
child.postMessage()發(fā)送消息,子窗口通過(guò)window.opener.postMessage()回復(fù)消息,如果要跨域,可以通過(guò) postMessage 第二參數(shù)指定目標(biāo)源,*代表向所有源發(fā)送消息
// 父窗口
const child = window.open('/some-page')
// 給子窗口定義唯一id,便于通信
child.name = 'win_' + id()
window.addEventlistener('message',()=>{
// 監(jiān)聽(tīng)子窗口傳遞的消息
})
// 在子窗口 /some-page 頁(yè)面
// 發(fā)送數(shù)據(jù)到父窗口
window.opener.postMessage({type:'message', name:window.name, data})
window.addEventlistener('message',()=>{
// 監(jiān)聽(tīng)父窗口傳遞的消息
})
Websocket
通過(guò) websocket ,以服務(wù)端做中轉(zhuǎn),實(shí)現(xiàn)跨域通信方案
- 支持跨域,同時(shí)支持跨瀏覽器跨設(shè)備
- 可以結(jié)合其他跨標(biāo)簽方案,實(shí)現(xiàn)復(fù)用 websocket 連接的操作
存在問(wèn)題:
- 需要服務(wù)端支持,實(shí)現(xiàn)較為復(fù)雜
MessageChannel
這個(gè)方案還是比較常用在 iframe 跨域通信機(jī)制中,僅記錄
MessageChannel 本質(zhì)也是依賴 MessagePort 來(lái)實(shí)現(xiàn)的通信機(jī)制,本質(zhì)和 SharedWorker 一樣
如果實(shí)現(xiàn)跨標(biāo)簽頁(yè)通訊,需要有中間人且支持結(jié)構(gòu)化克隆算法來(lái)協(xié)助傳遞 port 端口信息
