Electron多标签页实现:基于iframe的通信方案与局限
本文详解在Electron中实现多标签页的一种基于iframe的替代方案。针对官方推荐的BrowserView和WebContentsView因更新或兼容性导致的开发困境,文章展示了如何通过iframe结合postMessage机制,构建主进程、渲染进程与子页面间的双向通信链路。内容涵盖消息封装、刷新逻辑及登出流程,并指出了该方案在涉及第三方页面时因同源策略导致的通信局限。
Electron多标签页实现背景
前文已介绍如何在Electron中优雅地进行进程间通信,本文将重点探讨如何在Electron中实现多标签页模式,具体效果可参考下图。

尽管Electron已发展多年,但令人意外的是,目前尚未出现成熟且广泛使用的多标签页组件库。GitHub上Star数最高的“Tab component for Electron”已停止维护,且其实现依赖于Electron官方建议弃用的WebView(Web嵌入 | Electron)。后续虽有基于BrowserView的实现,但Electron官方目前更推荐使用WebContentsView。由于项目时间紧迫,未深入调研最新API,因此采用了一种较为基础的方案——利用iframe自行构建多标签页功能。
基于iframe的结构设计
首先查看HTML的结构,如下所示。

核心逻辑很简单,即每个标签页对应一个iframe元素。
多进程通信机制
界面实现相对简单,难点在于主进程、渲染进程(即包含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]; // 执行完回调删除
}
}
});
};
同时,渲染进程也可主动向iframe发送消息:
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进行标识,发送消息时只需指定当前激活标签页对应的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页面,实现了完整的双向通信。若未做好通信封装,处理此类逻辑将极为繁琐;而通过现有封装,开发者仅需关注业务代码本身。
以下是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多标签页相关内容,建议查阅本站过往文章或继续浏览后续系列教程。感谢大家的支持!
Mock.js 拦截失效或数据格式错误的常见原因及排查
2026-11-05 10:11:59
Java和JavaScript名字像但没关系:辨析Java、JavaWeb与JS
2026-11-04 09:30:54
CodePen、JSFiddle与RunJS对比:哪款JS在线运行工具更适合你?
2026-11-03 16:46:57
JS 统计图渲染空白或报错怎么排查?常见问题解决指南
2026-11-03 09:49:40
flv.js 常见报错 MediaError 与解码失败排查指南
2026-11-02 10:29:29
