Electron實(shí)現(xiàn)多標(biāo)簽頁模式詳解
上文介紹了 如何在 Electron 中優(yōu)雅的進(jìn)行進(jìn)程間通訊,接下來說說如何在 Electron 實(shí)現(xiàn)多標(biāo)簽頁模式,如下圖。
Electron 都發(fā)展這么多年了,讓人想不到的是,要實(shí)現(xiàn)一個(gè)多標(biāo)簽頁的功能居然沒有能用的輪子。能在 Github 上找到 Star 最多的一個(gè)輪子(Tab component for Electron)也已經(jīng)不再更新,而且還是使用 Electron 建議不再使用的 WebView 實(shí)現(xiàn)的(Web 嵌入 | Electron)。后面也有人基于 BrowserView 實(shí)現(xiàn)了一套,但是現(xiàn)在 Electron 又不推薦使用 BrowserView 了,建議使用 WebContentsView。因?yàn)轫?xiàng)目比較急,沒有花太多時(shí)間去研究了,就用比較 low 的方案 - iframe 自己搓了一個(gè)。
直接看 HTML 的結(jié)構(gòu)吧,如下
也就是一個(gè) tab 對(duì)應(yīng)一個(gè) iframe。
界面沒啥好說的,稍微有點(diǎn)復(fù)雜的就是主進(jìn)程、渲染進(jìn)程(iframe 所在的頁面)、iframe 之間的通訊。
在實(shí)際的業(yè)務(wù)場(chǎng)景中,關(guān)閉窗口的時(shí)候需要彈框讓用戶確認(rèn)、用戶確認(rèn)后 iframe 里的頁面需要調(diào)接口進(jìn)行登出,然后通知主進(jìn)程關(guān)閉窗口。整個(gè)消息鏈路涉及了主進(jìn)程、渲染進(jìn)程、iframe 頁面,而且還是雙向的。
上文已經(jīng)講了如何封裝主進(jìn)程、渲染進(jìn)程之間的通訊,下面講講渲染進(jìn)程(iframe 所在的頁面)、iframe 之間的通訊。
渲染進(jìn)程監(jiān)聽消息、處理消息:
export const addIframeWebEventListener = () => { window.addEventListener("message", async (event) => { const message = event.data as { iframeWebCmd: string; cbid: string; code: number; data: never; }; if (message.iframeWebCmd) { console.log(message); if (message.iframeWebCmd !== "postMessageCallback") { if (handle[message.iframeWebCmd]) { try { const res = await handle[message.iframeWebCmd](message.data); invokeCallback(message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(message.cbid, ex); } } else { invokeErrorCallback( message.cbid, `方法不存在:${message.iframeWebCmd}`, ); } } else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 } } }); };
渲染進(jìn)程主動(dòng)發(fā)送消息:
function postMessage( data: { electronWebCmd: string; data?: any }, cb?: (data: any) => void, errorCb?: (data: any) => void, ) { const iframe = document.getElementById( tabStore.currentTabId.value!, ) as HTMLIFrameElement; if (cb) { const cbid = Date.now(); callbacks[cbid] = cb; iframe?.contentWindow?.postMessage( { ...data, cbid, }, "*", ); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { iframe?.contentWindow?.postMessage(data, "*"); } } export function request<T = unknown>(params: { cmd: string; data?: any }) { return new Promise<T>((resolve, reject) => { postMessage( { electronWebCmd: params.cmd, data: params.data }, (res) => { resolve(res); }, (error) => { reject(error); }, ); }); }
每一個(gè) iframe 都使用了 id 進(jìn)行標(biāo)識(shí),發(fā)送消息就是給當(dāng)前激活的 tab 對(duì)應(yīng)的 iframe 發(fā)消息。
當(dāng)需要渲染進(jìn)程給 iframe 發(fā)消息的時(shí)候,就可以像調(diào)用 HTTP 請(qǐng)求一樣發(fā)送消息,比如讓 iframe 頁面進(jìn)行刷新:
export function refresh() { return request({ cmd: "refresh", }); }
完整代碼:
/* eslint-disable no-case-declarations */ /* eslint-disable no-shadow */ import { useTabsStore } from "@/store/tabs"; import handle from "./handle"; /* eslint-disable @typescript-eslint/no-explicit-any */ const callbacks: { [propName: string]: (data: any) => void } = {}; const errorCallbacks: { [propName: string]: (data: any) => void } = {}; const tabStore = useTabsStore(); function postMessage( data: { electronWebCmd: string; data?: any }, cb?: (data: any) => void, errorCb?: (data: any) => void, ) { const iframe = document.getElementById( tabStore.currentTabId.value!, ) as HTMLIFrameElement; if (cb) { const cbid = Date.now(); callbacks[cbid] = cb; iframe?.contentWindow?.postMessage( { ...data, cbid, }, "*", ); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { iframe?.contentWindow?.postMessage(data, "*"); } } export function request<T = unknown>(params: { cmd: string; data?: any }) { return new Promise<T>((resolve, reject) => { postMessage( { electronWebCmd: params.cmd, data: params.data }, (res) => { resolve(res); }, (error) => { reject(error); }, ); }); } function invokeCallback<T = unknown>(cbid: string, res: T) { ( document.getElementById(tabStore.currentTabId.value!) as HTMLIFrameElement )?.contentWindow?.postMessage( { electronWebCmd: "postMessageCallback", cbid, data: res, code: 200, }, "*", ); } function invokeErrorCallback(cbid: string, res: unknown) { ( document.getElementById(tabStore.currentTabId.value!) as HTMLIFrameElement )?.contentWindow?.postMessage( { electronWebCmd: "postMessageCallback", cbid, data: res, code: 400, }, "*", ); } export const addIframeWebEventListener = () => { window.addEventListener("message", async (event) => { const message = event.data as { iframeWebCmd: string; cbid: string; code: number; data: never; }; if (message.iframeWebCmd) { console.log(message); if (message.iframeWebCmd !== "postMessageCallback") { if (handle[message.iframeWebCmd]) { try { const res = await handle[message.iframeWebCmd](message.data); invokeCallback(message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(message.cbid, ex); } } else { invokeErrorCallback( message.cbid, `方法不存在:${message.iframeWebCmd}`, ); } } else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 } } }); };
iframe 頁面監(jiān)聽消息、處理消息:
export const addElectronWebWebEventListener = () => { window.addEventListener("message", async (event) => { const message = event.data as { electronWebCmd: string; cbid: string; code: number; data: never; }; if (message.electronWebCmd) { if (message.electronWebCmd !== "postMessageCallback") { if (handle[message.electronWebCmd]) { try { const res = await handle[message.electronWebCmd](message.data); invokeCallback(message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(message.cbid, ex); } } else { invokeErrorCallback( message.cbid, `方法不存在:${message.electronWebCmd}`, ); } } else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 } } }); };
iframe 發(fā)送消息:
function postMessage( data: { iframeWebCmd: string; data?: unknown }, cb?: (data: unknown) => void, errorCb?: (data: unknown) => void, ) { if (cb) { const cbid = Date.now(); callbacks[cbid] = cb; window.parent?.postMessage( { ...data, cbid, }, "*", ); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { window.parent?.postMessage(data, "*"); } } export function request<T = unknown>(params: { cmd: string; data?: unknown }) { return new Promise<T>((resolve, reject) => { postMessage( { iframeWebCmd: params.cmd, data: params.data }, (res) => { resolve(res as T); }, (error) => { reject(error); }, ); }); }
如此一來 iframe 頁面發(fā)消息的時(shí)候也很簡(jiǎn)單:
/** * @description 獲取 mac 地址 * @returns */ export const getMac = () => { return request<string>({ cmd: "getMac", }); };
獲取 mac 地址,消息的傳遞過程是:iframe 頁面 -> 渲染進(jìn)程 -> 主進(jìn)程,主進(jìn)程 -> 渲染進(jìn)程 -> iframe 頁面,屬于雙向通訊。如果沒有做好通訊的封裝,處理起來想想都麻煩,而現(xiàn)在只需要關(guān)注業(yè)務(wù)代碼就好了。
完整代碼:
import handle from "./handle"; /* eslint-disable no-shadow */ const callbacks: { [propName: string]: (data: unknown) => void } = {}; const errorCallbacks: { [propName: string]: (data: unknown) => void } = {}; function postMessage( data: { iframeWebCmd: string; data?: unknown }, cb?: (data: unknown) => void, errorCb?: (data: unknown) => void, ) { if (cb) { const cbid = Date.now(); callbacks[cbid] = cb; window.parent?.postMessage( { ...data, cbid, }, "*", ); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { window.parent?.postMessage(data, "*"); } } export function request<T = unknown>(params: { cmd: string; data?: unknown }) { return new Promise<T>((resolve, reject) => { postMessage( { iframeWebCmd: params.cmd, data: params.data }, (res) => { resolve(res as T); }, (error) => { reject(error); }, ); }); } function invokeCallback<T = unknown>(cbid: string, res: T) { window.parent?.postMessage( { iframeWebCmd: "postMessageCallback", cbid, data: res, code: 200, }, "*", ); } function invokeErrorCallback(cbid: string, res: unknown) { window.parent?.postMessage( { iframeWebCmd: "postMessageCallback", cbid, data: res, code: 400, }, "*", ); } export const addElectronWebWebEventListener = () => { window.addEventListener("message", async (event) => { const message = event.data as { electronWebCmd: string; cbid: string; code: number; data: never; }; if (message.electronWebCmd) { if (message.electronWebCmd !== "postMessageCallback") { if (handle[message.electronWebCmd]) { try { const res = await handle[message.electronWebCmd](message.data); invokeCallback(message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(message.cbid, ex); } } else { invokeErrorCallback( message.cbid, `方法不存在:${message.electronWebCmd}`, ); } } else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除 } } }); };
在 Electron 里基于 iframe 的方案實(shí)現(xiàn)多標(biāo)簽頁,有一個(gè)致命的缺陷就是,如果 iframe 里的頁面屬于第三方,那么就無法與里面的頁面進(jìn)行同通訊,比如我在實(shí)現(xiàn)刷新標(biāo)簽頁的時(shí)候,是給 iframe 里的頁面發(fā)送消息,頁面收到消息后執(zhí)行下面的代碼:
refresh: () => { const iframeID = getIframeId(); if (iframeID) { let href = location.href; if (href.indexOf("?") === -1) { href = href + `?iframeId=${iframeID}`; } else { if (href.indexOf("iframeId") === -1) { href = href + `&iframeId=${iframeID}`; } } location.href = href; setTimeout(() => { location.reload(); }, 500); } else { location.reload(); } }
到此這篇關(guān)于Electron實(shí)現(xiàn)多標(biāo)簽頁模式詳解的文章就介紹到這了,更多相關(guān)Electron多標(biāo)簽頁模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)的自定義右鍵菜單實(shí)例二則
這篇文章主要介紹了JS實(shí)現(xiàn)的自定義右鍵菜單,以兩則實(shí)例形式分析了javascript自定義右鍵菜單效果的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09移動(dòng)端Ionic App 資訊上下循環(huán)滾動(dòng)的實(shí)現(xiàn)代碼(跑馬燈效果)
這篇文章主要介紹了移動(dòng)端Ionic App 資訊上下循環(huán)滾動(dòng)的實(shí)現(xiàn)代碼,實(shí)現(xiàn)方法需要借助jQuery庫的選擇器和動(dòng)畫函數(shù),并且把jquery的操作封裝到指令里,具體指令代碼大家通過本文學(xué)習(xí)吧2017-08-08JavaScript實(shí)現(xiàn)隨機(jī)點(diǎn)名器實(shí)例詳解
這篇文章主要介紹了JavaScript隨機(jī)點(diǎn)名器,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05JavaScript forEach()遍歷函數(shù)使用及介紹
這篇文章主要介紹了JavaScript forEach()遍歷函數(shù)使用及介紹,本文講解了使用forEach遍歷數(shù)組的用法以及提前終止循環(huán)的一個(gè)方法技巧,需要的朋友可以參考下2015-07-07一文帶你搞懂JS中導(dǎo)入模塊import和require的區(qū)別
JavaScript中,模塊是一種可重用的代碼塊,它將一些代碼打包成一個(gè)單獨(dú)的單元,并且可以在其他代碼中進(jìn)行導(dǎo)入和使用。JavaScript中有兩種常用的方式:使用import和require,本文主要聊聊他們二者的區(qū)別2023-03-03原生js實(shí)現(xiàn)的貪吃蛇網(wǎng)頁版游戲完整實(shí)例
這篇文章主要介紹了原生js實(shí)現(xiàn)的貪吃蛇網(wǎng)頁版游戲完整實(shí)例,可實(shí)現(xiàn)自主選擇游戲難度進(jìn)行貪吃蛇游戲的功能,涉及javascript鍵盤事件及頁面元素的操作技巧,需要的朋友可以參考下2015-05-05JavaScript實(shí)現(xiàn)獲取img的原始尺寸的方法詳解
在微信小程序開發(fā)時(shí),它的image標(biāo)簽有一個(gè)默認(rèn)高度,這樣你的圖片很可能出現(xiàn)被壓縮變形的情況,所以就需要獲取到圖片的原始尺寸對(duì)image的寬高設(shè)置,本文就來分享一下JavaScript實(shí)現(xiàn)獲取img的原始尺寸的方法吧2023-03-03