Electron实现多标签页模式详解
作者:若邪
上文介绍了 如何在 Electron 中优雅的进行进程间通讯,接下来说说如何在 Electron 实现多标签页模式,如下图。
Electron 都发展这么多年了,让人想不到的是,要实现一个多标签页的功能居然没有能用的轮子。能在 Github 上找到 Star 最多的一个轮子(Tab component for Electron)也已经不再更新,而且还是使用 Electron 建议不再使用的 WebView 实现的(Web 嵌入 | Electron)。后面也有人基于 BrowserView 实现了一套,但是现在 Electron 又不推荐使用 BrowserView 了,建议使用 WebContentsView。因为项目比较急,没有花太多时间去研究了,就用比较 low 的方案 - iframe 自己搓了一个。
直接看 HTML 的结构吧,如下
也就是一个 tab 对应一个 iframe。
界面没啥好说的,稍微有点复杂的就是主进程、渲染进程(iframe 所在的页面)、iframe 之间的通讯。
在实际的业务场景中,关闭窗口的时候需要弹框让用户确认、用户确认后 iframe 里的页面需要调接口进行登出,然后通知主进程关闭窗口。整个消息链路涉及了主进程、渲染进程、iframe 页面,而且还是双向的。
上文已经讲了如何封装主进程、渲染进程之间的通讯,下面讲讲渲染进程(iframe 所在的页面)、iframe 之间的通讯。
渲染进程监听消息、处理消息:
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]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } } }); };
渲染进程主动发送消息:
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); }, ); }); }
每一个 iframe 都使用了 id 进行标识,发送消息就是给当前激活的 tab 对应的 iframe 发消息。
当需要渲染进程给 iframe 发消息的时候,就可以像调用 HTTP 请求一样发送消息,比如让 iframe 页面进行刷新:
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]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } } }); };
iframe 页面监听消息、处理消息:
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]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } } }); };
iframe 发送消息:
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 页面发消息的时候也很简单:
/** * @description 获取 mac 地址 * @returns */ export const getMac = () => { return request<string>({ cmd: "getMac", }); };
获取 mac 地址,消息的传递过程是:iframe 页面 -> 渲染进程 -> 主进程,主进程 -> 渲染进程 -> iframe 页面,属于双向通讯。如果没有做好通讯的封装,处理起来想想都麻烦,而现在只需要关注业务代码就好了。
完整代码:
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]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } } }); };
在 Electron 里基于 iframe 的方案实现多标签页,有一个致命的缺陷就是,如果 iframe 里的页面属于第三方,那么就无法与里面的页面进行同通讯,比如我在实现刷新标签页的时候,是给 iframe 里的页面发送消息,页面收到消息后执行下面的代码:
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(); } }
到此这篇关于Electron实现多标签页模式详解的文章就介绍到这了,更多相关Electron多标签页模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!