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 對應(yīng)一個(gè) iframe。
界面沒啥好說的,稍微有點(diǎn)復(fù)雜的就是主進(jìn)程、渲染進(jìn)程(iframe 所在的頁面)、iframe 之間的通訊。
在實(shí)際的業(yè)務(wù)場景中,關(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)識,發(fā)送消息就是給當(dāng)前激活的 tab 對應(yīng)的 iframe 發(fā)消息。
當(dāng)需要渲染進(jìn)程給 iframe 發(fā)消息的時(shí)候,就可以像調(diào)用 HTTP 請求一樣發(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í)候也很簡單:
/**
* @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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js中g(shù)etter和setter用法實(shí)例分析
這篇文章主要介紹了js中g(shù)etter和setter用法,結(jié)合實(shí)例形式分析了javascript中g(shù)etter和setter的功能、使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2018-08-08
JavaScript中具名函數(shù)的多種調(diào)用方式總結(jié)
這篇文章主要介紹了JavaScript中具名函數(shù)的多種調(diào)用方式總結(jié),本文總結(jié)了4種方法,需要的朋友可以參考下2014-11-11
JavaScript實(shí)現(xiàn)H5接金幣功能(實(shí)例代碼)
這篇文章主要介紹了JavaScript實(shí)現(xiàn)H5接金幣功能,本文分步驟通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
js實(shí)現(xiàn)HashTable(哈希表)的實(shí)例分析
本文詳細(xì)介紹javascript哈希表的實(shí)例分析及用法。下面就跟小編一起來學(xué)習(xí)下吧2016-11-11
js實(shí)現(xiàn)點(diǎn)擊左右按鈕輪播圖片效果實(shí)例
這篇文章主要介紹了js實(shí)現(xiàn)點(diǎn)擊左右按鈕輪播圖片效果的方法,涉及click事件相應(yīng)、animate方法等使用技巧,需要的朋友可以參考下2015-01-01
用js+iframe形成頁面的一種遮罩效果的具體實(shí)現(xiàn)
用js形成頁面的一種遮罩效果,選擇想要進(jìn)行遮罩的窗口,在這里想要遮罩的是一個(gè)iframe窗口,具體的實(shí)現(xiàn)如下,感興趣的朋友可以參參考下2013-12-12

