瀏覽器跨 Tab 窗口通信原理及應用實踐
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
最近,相信大家一定被這么個動效給刷屏了: 以至于,基于這個效果的二次創作層出不窮,眼花繚亂。 基于跨窗口通信的彈彈球: 基于跨窗口通信的 Flippy Bird: 我也嘗試制作了一個跨 Tab 窗口的 CSS 動畫聯動,效果如下: 代碼不多,核心代碼 200 行,感興趣的可以戳這里:Github - broadcastAnimation 當然,本文的核心不是去一一剖析上面的效果具體的實現方式,而是講講其中比較關鍵的一個技術點: 而是應用如何在多窗口下進行互相通信。 所謂多窗口下進行互相通信,是指在瀏覽器中,不同窗口(包括不同標簽頁、不同瀏覽器窗口甚至不同瀏覽器實例)之間進行數據傳輸和通信的能力。 當然,本文我們探討的是純前端的跨 Tab 頁面通信,在非純前端的方式下,我們可以借助諸如 Web Socket 等方式,藉由后端這個中間載體,進行跨頁面通信。 因此,本文我們更多的重心將放在,如何基于純前端技術,實現多窗口下進行互相通信。 為了實現跨窗口通信,它應該需要具備以下能力:
方式一:Broadcast Channel()Broadcast Channel 是一個較新的 Web API,用于在不同的瀏覽器窗口、標簽頁或框架之間實現跨窗口通信。它基于發布-訂閱模式,允許一個窗口發送消息,并由其他窗口接收。 其核心步驟如下:
同時,Broadcast Channel 遵循瀏覽器的同源策略。這意味著只有在同一個協議、主機和端口下的窗口才能正常進行通信。如果窗口不滿足同源策略,將無法互相發送和接收消息。 因為有同源限制,我們需要起一個服務,這里我基于 Vite 快速起了一個 Vue 項目,簡單的基于 其核心代碼非常簡單: <template> <div id="j-main"> // ... </div> </template> <script> import { onMounted } from 'vue'; export default { setup() { function createBroadcastChannel() { broadcastChannel = new BroadcastChannel('broadcast'); broadcastChannel.onmessage = handleMessage; } function sendMessage(data) { broadcastChannel.postMessage(data); } function handleMessage(event) { console.log('接收到 event', event); // TODO: 處理接收到信息后的邏輯 } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); sendMessage(pos); }); } // 計算當前元素距離顯示器窗口右上角的距離 function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 獲取元素相對于屏幕左上角的 X 和 Y 坐標 const x = rect.left + window.screenX; // 元素左邊緣相對于屏幕左邊緣的距離 const y = rect.top + window.screenY + barHeight;// 元素頂部邊緣相對于屏幕頂部邊緣的距離 return [x, y]; }
onMounted(() => { createBroadcastChannel(); resizeEventBind(); }); return {}; } }; </script> <style lang="scss"></style> 這里,我們的核心邏輯在于:
在 這樣,當我們同時打開兩個窗口,移動其中一個窗口,就可以向另外一個窗口發生當前窗口希望傳遞過去的信息,在本例子中就是 假設 可以看到,如果我們同時打開兩個一個的頁面,當觸發右邊頁面的 Resize,左邊的頁面會收到基于 而一個完整的 Event 信息如下: 譬如,傳遞過來的信息放在 data 屬性內、同時也可以獲取當前的的 Broadcast Name 等。 基于 BroadcastChannel,就可以實現每個 Tab 內的核心信息互傳, 可以得知當前在線設備數,再基于這些信息去完成我們想要的動畫、交互等效果。 這里的核心點,還是:
其本質就是一個數據共享池子。 方式二:SharedWorker API好,介紹完 Broadcast Channel(),我們再來看看 SharedWorker API。 SharedWorkerAPI 是 HTML5 中提供的一種多線程解決方案,它可以在多個瀏覽器 TAB 頁面之間共享一個后臺線程,從而實現跨頁面通信。 與其他 Worker 不同的是,SharedWorker 可以被多個瀏覽器 TAB 頁面共享,且可以在同一域名下的不同頁面之間建立連接。這意味著,多個頁面可以通過 SharedWorker 實例之間的消息傳遞,實現跨 TAB 頁面的通信。 它的實現與上面的 Broadcast Channel 非常類似,我們來看一看實際的代碼: <template> <div id="j-main"> // ... </div> </template> <script> import { onMounted } from 'vue'; export default { setup() { // 創建一個 SharedWorker 對象 let worker;
function initWorker() { // 創建一個 SharedWorker 對象 worker = new SharedWorker('/shared-worker.js', 'tabWorker'); // 監聽消息事件 worker.port.onmessage = function (event) { console.log('接收到 event', event); handleMessage(event); }; }
function handleMessage(data) { // TODO: 處理接收到信息后的邏輯 } function sendMessage(data) { // 發送消息 worker.port.postMessage(data); } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); sendMessage(pos); }); } function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 獲取元素相對于屏幕左上角的 X 和 Y 坐標 const x = rect.left + window.screenX; // 元素左邊緣相對于屏幕左邊緣的距離 const y = rect.top + window.screenY + barHeight;// 元素頂部邊緣相對于屏幕頂部邊緣的距離 return [x, y]; }
onMounted(() => { initWorker(); resizeEventBind(); }); return {}; } }; </script> <style></style> 簡單描述一下,上面也說了,跨 Tab 頁通信的核心在于數據向外的發送與接收的能力:
當然,上面有引入一個 //shared-worker.js const connections = []; onconnect = function (event) { var port = event.ports[0]; connections.push(port); port.onmessage = function (event) { // 接收到消息時,向所有連接發送該消息 connections.forEach(function (conn) { if (conn !== port) { conn.postMessage(event.data); } }); }; port.start(); }; 簡單解析一下,下面對其進行解析:
總而言之,shared-worker.js 腳本創建了一個共享 Worker 實例,它可以接收來自不同頁面的連接請求,并將接收到的消息發送給其他連接的頁面。通過使用 SharedWorker API,實現跨 TAB 頁面之間的通信和數據共享。 同理,我們來看看基于 Worker 的數據傳輸效果,同樣是簡化 DEMO,當 Resize 窗口時,向另外一個窗口發送當前窗口下 可以看到,如果我們同時打開兩個一個的頁面,當觸發右邊頁面的 Resize,左邊的頁面會利用 而一個完整的 Event 信息如下: 可以看到,在 SharedWorker 方式中,傳輸數據與 Broadcast Channel 是一樣的,都是利用
兼容性方面,到今天(2023-11-26),broadcast Channel 看著是兼容性更好的方式: 另外,需要注意的是,兩個方法都使用了 但是,單獨使用 方式三:localStorage/sessionStorageOK,最后一種跨 Tab 窗口通信的方式是利用 與上面 Broadcast Channel、SharedWorker 稍微不同的地方在于:
簡單看看代碼: <template> <div id="j-main"> // ... </div> </template> <script> import { ref, reactive, computed, onMounted } from 'vue'; export default { setup() { function initLocalStorage() { let tabArray = JSON.parse(localStorage.getItem('tab_array')); if (!tabArray) { const tabIndex = 1; id = tabIndex; localStorage.setItem('tab_array', JSON.stringify([tabIndex])); } else { const tabIndex = tabArray[tabArray.length - 1] + 1; id = tabIndex; const newTabArray = [...tabArray, tabIndex]; localStorage.setItem('tab_array', JSON.stringify(newTabArray)); } } function setLocalStorage(data) { localStorage.setItem(`tab_index_${id}`, JSON.stringify(data)); } function handleMessage(data) { const rArray = JSON.parse(data); remoteX.value = rArray[0]; remoteY.value = rArray[1]; } function resizeEventBind() { window.addEventListener('resize', () => { const pos = getCurPos(); setLocalStorage(pos); });
window.addEventListener('storage', (event) => { console.log('localStorage 變化了!', event); console.log('鍵名:', event.key); console.log('變化前的值:', event.oldValue); console.log('變化后的值:', event.newValue); handleMessage(event.newValue); }); } function getCurPos() { const barHeight = window.outerHeight - window.innerHeight; const element = document.getElementById('j-main'); const rect = element.getBoundingClientRect(); // 獲取元素相對于屏幕左上角的 X 和 Y 坐標 const x = rect.left + window.screenX; // 元素左邊緣相對于屏幕左邊緣的距離 const y = rect.top + window.screenY + barHeight;// 元素頂部邊緣相對于屏幕頂部邊緣的距離 return [x, y]; }
onMounted(() => { initLocalStorage(); resizeEventBind(); }); return {}; } }; </script> <style></style> 同樣的簡單解析一下:
交互傳輸結果,與上述兩個動圖是一致的,就不額外貼圖了,但是基于 我們通過 當然,由于 雖然看起來這種方式最不優雅,但是結合兼容性一起看, localstorage 反而是兼容性最好的方式。在數據量較小的時候,性能相差不會太大,反而可能是更好的選擇。 我基于上面三種方式:Broadcast Channel、SharedWorker 與 localStorage,都實現了一遍下面這個跨 Tab 頁的 CSS 聯動動畫: 三種方式的代碼都不多,感興趣的可以戳這里:Github - broadcastAnimation 實際應用思考當然,上面的實現其實有很大一個瑕疵。 那就是我們只顧著實現通信,沒有考慮實際應用中的一些實際問題:
基于實際應用,我們需要基于上述 3 種方式,進一步細化方案。 上面,為了方便演示,每次傳輸數據時,只傳輸動畫需要的數據。而實際應用,我們可以需要細化整個傳輸數據,設定合理的協議。譬如: { // 傳輸狀態: // 1 - 首次傳輸 // 2 - 正常通信 // 3 - 頁面關閉 status: 1 | 2 | 3, data: {} } 接收方需要基于收到信息所展示的不同的狀態,做出不同的反饋。 當然,還有一個問題,我們如何知道頁面被關閉了?基于組件的 這些信息都有可能因為 Tab 頁面失活,導致關閉的信息無法正常被發送出去。所以,實際應用中,我們經常用的一項技術是心跳上報/心跳廣播,一旦建立連接后,間隔 X 秒發送一次心跳廣播,告訴其他接收端,我還在線。一旦超過某個時間閾值沒有收到心跳上報,各個訂閱方可以認為該設備已經下線。 總而言之,跨 Tab 窗口通信應用在實際應用的過程中,我們需要思考更多可能隱藏的問題。 跨 Tab 窗口通信應用場景當然,除了最近大火的跨 Tab 動畫應用場景,實際業務中,還有許多場景是它可以發揮作用的。這些場景利用了跨 Tab 通信技術,增強了用戶體驗并提供了更豐富的功能。 以下是一些常見的應用場景:
譬如這個:
舉幾個實際的例子:
總之,跨 Tab 窗口通信在實時協作、數據同步、通知提醒等方面都能發揮重要作用,為用戶提供更流暢、便捷的交互體驗。 最后本文只羅列了 3 種較為常見,適用性強的方式。除去本文羅列的方式,肯定還有其他方式能夠實現跨 Tab 通信。 譬如,基于 Window: opener property 配合 更多有意思的方式,期待大家的補充與探索。 好了,本文到此結束,希望對你有幫助 😃 作者:ChokCoco 來源鏈接:https://www.cnblogs.com/coco1s/p/17861360.html 該文章在 2023/11/28 15:03:45 編輯過 |
關鍵字查詢
相關文章
正在查詢... |