Electron 进程通信 IPC(Inter-Process Communication)
前言
首先大家需要了解一些 Electron 的前置知识。众所周知 Electron 是将 chrome Chromium 内核 和 Node.js 一起嵌入到一个二进制文件中来实现构建跨平台桌面应用的框架。其中 Chromium 提供 WebAPI(DOM、BOM) 的能力,Node.js 提供了调用 系统 API 的能力。
什么是进程?
进程(Process) 是操作系统中一个核心概念,用于描述程序的动态执行过程。它代表了程序在计算机内存中的一次运行实例,是操作系统进行资源分配和调度的基本单位。
上面的解释取自文心一言,我理解的进程便是 "微信" 与 "QQ" 的关系,两个应用便是两个进程。而操作系统便是他们的管理员,对它们进行资源分配和调度。
chromium 为什么是多进程架构?
Chromium 的多进程架构通过隔离故障、限制权限、并行计算和动态资源管理,实现了浏览器在稳定性、安全性、性能和资源利崩溃的平衡,尤其适应现代 Web 的复杂需求。
1. 稳定性保障
- 崩溃隔离:每个标签页或插件运行在独立进程,单进程崩溃不影响其他页面或浏览器整体。
- 内存泄漏可控:渲染进程内存泄漏仅影响自身,关闭进程即可释放资源,避免全局卡顿。
2. 安全性强化
- 沙盒机制:渲染和插件进程在沙盒中运行,权限受限,恶意代码无法直接攻击系统。
- 站点隔离:不同域名网页分配独立进程,防止跨站攻击(如 Spectre 等漏洞利用)。
3. 性能优化
- 并行计算:多进程利用多核 CPU,GPU 进程独立加速渲染,提升复杂页面(如视频、游戏)性能。
- 资源动态分配:根据进程优先级(如最近使用标签页)动态调整内存,减少磁盘交换,提升响应速度。
4. 资源灵活管理
- 进程模型可配置:支持单进程(低资源设备)到多进程(高性能设备)的灵活切换。
- 进程复用策略:相同域名页面共享进程,避免进程数量激增;进程超载时合并资源,平衡负载。
Electron 的进程模型
Electron 继承了 Chromium 的多进程架构,这使得该框架在架构上与现代 Web 浏览器非常相似。在 Electron 应用中主要分为 主进程(main) 跟 渲染进程(renderer),他们分别为:
- 主进程 :作为 Electron 应用的入口进程,它运行在 Node 环境中,因此它可以作为一个服务器给渲染进程提供调用系统级 API 的支持。主进程的主要目的是使用
BrowserWindow
模块创建和管理应用窗口。 - 渲染进程 :每个 Electron 应用都会为每个打开的
BrowserWindow
(以及每个 Web 嵌入)生成一个单独的渲染进程。顾名思义,渲染器负责渲染 Web 内容。可以调用 chromium 提供的 WebAPI,它跟一个 chrome 浏览器没有任何区别,所以渲染进程中运行的代码的行为应符合 Web 标准。
可以理解为每个渲染进程都是一个独立的浏览器窗口,它们具备
chromium
浏览器所有的功能。
安全问题
是否开启 Node 集成(nodeIntegration)
BrowserWindow.webPreferences.nodeIntegration: true
时,为渲染进程集成 Node API。默认:false

这是极其不推荐且非常不安全的做法,虽然它为我们在渲染进程操作系统级 API 带来了便利,但是相对应的也为恶意代码留下了可乘之机。例如:
js// 假设存在 XSS 漏洞,攻击者注入以下代码: const fs = require('fs'); fs.writeFileSync('/tmp/malicious.sh', '恶意脚本内容'); const { exec } = require('child_process'); exec('chmod +x /tmp/malicious.sh && /tmp/malicious.sh');
上下文隔离(contextIsolation)
上下文隔离是一项功能,可确保你的 preload
脚本和 Electron 的内部逻辑在与你在 webContents
中加载的网站不同的上下文中运行。这对于安全目的很重要,因为它有助于防止网站访问 Electron 内部结构或预加载脚本有权访问的强大 API。
这意味着你的预加载脚本有权访问的 window
对象实际上与网站有权访问的对象不同。例如,如果你在预加载脚本中设置了 window.hello = 'wave'
并启用了上下文隔离,则当网站尝试访问 window.hello
时,window.hello
将是未定义的。
自 Electron 12 起,上下文隔离已默认启用,并且它是所有应用的推荐安全设置。
上文我是直接粘贴官网的解释。简单来说就是
renderer.js
中的window
与preload.js
中的window
是否共享。
示例:
- 创建
src/typings/global.d.ts
, 为 global 对象声明全局变量username
。 - 创建
electron/preload.ts
, 设置window.username = 'adimn'
。 - 在
main.ts
中配置BrowserWindow.webPreferences.preload: path.join(__dirname, "preload.js")
。 App.vue
中打印console.log(window.username)
。vite.electron.config.ts
修改electron
插件配置 为electron([{ entry: "electron/main.ts" }, { entry: "electron/preload.ts" }])


打印结果为 admin
,证明 preload
跟 renderer
使用的是同一个 window
对象。
- 上述代码不变,仅修改
contextIsolation: true
时:

可以看到打印是 undefined
也就证明了此时 window
是各自独立的。也就是文中所说的 是否共享 window
。
预加载脚本(preload)
出于安全原因,渲染进程默认只运行网页不运行 Node.js。所以为了将 Electron 的不同进程桥接在一起,我们需要使用一个称为预加载的特殊脚本。总而言之 preload
是 Electron
提供的一个可以安全访问 系统 API 的沙盒环境,是渲染进程安全访问 系统API
的桥梁。
preload
脚本中可以访问WebAPI
以及有限的Node.js
和Electron API
。
从 Electron 20 开始,预加载脚本默认被沙箱化,并且不再能够访问完整的 Node.js 环境。
可用的 API 细节 Electron 模块 渲染进程模块 Node.js API events
、url
、timers
Polyfill 全局变量 Buffer
、process
、clearImmediate
、setImmediate
contextBridge
在 preload
脚本中,使用 contextBridge.exposeInMainWorld(apiKey, api)
或 contextBridge.exposeInIsolatedWorld(worldId,apiKey, api)
方法将 api 暴露给渲染进程。
注意:即便是开启了上下文隔离 跟关闭Node 集成 也需要规范小心的使用
contextBridge Api
才能保证安全的将接口提供给渲染进程使用。例如:
preload.ts// ❌ 错误做法 contextBridge.exposeInMainWorld('myAPI', { send: ipcRenderer.send }) // ✅ 正确做法 contextBridge.exposeInMainWorld('myAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs') })
第一种方式直接公开强大的 API,无需任何类型的参数过滤。这将允许任何网站发送任意 IPC 消息,而你不希望这种情况发生。公开基于 IPC 的 API 的正确方法是为每个 IPC 消息提供一种方法。
进程通信
进程间通信(IPC)是在 Electron 中构建功能丰富的桌面应用的关键部分。由于主进程和渲染进程在 Electron 的进程模型中具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改等。
-
ipcMain :
ipcMain
模块是 Event Emitter。 当在主进程中使用时,它处理从渲染器进程(网页)发送的异步和同步消息。从渲染器发送的消息将被发送到该模块 -
ipcRenderer :
ipcRenderer
模块是 EventEmitter。它提供了一些方法,以便你可以从渲染进程(网页)向主进程发送同步和异步消息。你还可以接收来自主进程的响应
进程通信的四种模式
根据 ipcRenderer.send
、ipcMain.on
、ipcRenderer.invoke
、ipcMain.handle
以及 MessageChannel API 可以组合为 4 种基本的通信模式。
一、 渲染进程 ==> 主进程(单向)
这种模式通常是渲染进程需要使用主进程系统 api 的能力来完成一些需求,且无需等待主进程的执行结果时使用。例如下面一个动态修改窗口 title 的示例。
- 在主进程中添加
ipcMain.on(channel, callback)
方法监听渲染进程发送的消息动态修改标题。 - 使用
ipcRenderer.send(channel, ...args)
方法发送消息通知主进程修改标题。
注意:所有在
renderer
中调用的 API 都是在contextIsolation: true
以及 未开启nodeIntegration: false
的沙箱环境下在preload
中使用contextBridge
API 提供的。类型声明别漏了🫡
ts
# global.d.ts
export interface ElectronAPI {
setTitle(title: string): void;
}
declare global {
var electronAPI: ElectronAPI;
}
export {};
ts
# main.ts
import { ipcMain } from 'electron'
ipcMain.on("set-title", (event, title) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.setTitle(title);
});
ts
# preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title: string) => {
ipcRenderer.send('set-title', title)
}
})
ts
# App.vue
<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");
function setTitle() {
window.electronAPI.setTitle(title.value);
}
</script>
<template>
<input type="text" v-model="title" />
<button @click="setTitle">确认</button>
</template>

二、 主进程 <==> 渲染进程
双向 IPC 的常见应用场景通常是渲染进程需要使用主进程系统 API 的某些能力并等待返回结果时。通过使用 ipcRenderer.invoke
与 ipcMain上述andle
配对来完成。添加初始化时获取当前窗口 title 的示例:
- 在主进程使用
ipcMain.handle('get-title')
添加一个处理器等待ipcRenderer.invoke('get-title')
触发。 - 在 preload 脚本中 调用
ipcRenderer.invoke('get-title')
将getTitle
API 提供给 renderer - 在 renderer 中调用
window.electronAPI.getTitle()
获取当前窗口的默认标题。
ts
export interface ElectronAPI {
getTitle(): Promise<string>;
}
declare global {
var electronAPI: ElectronAPI;
}
export {};
ts
# main.ts
import { ipcMain } from "electron";
ipcMain.handle("get-title", (event) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
return win?.getTitle();
});
ts
# preload.ts
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("electronAPI", {
getTitle: () => {
return ipcRenderer.invoke("get-title");
}
});
ts
# App.vue
<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");
window.electronAPI.getTitle().then((value) => {
title.value = value;
});
</script>
<template>
<h1>{{ title }}</h1>
</template>
可以看到 h1
标签中显示的是 Vite + Vue + TS
,而不是默认的 App Title
,就证明我们已经通过主进程的 api 获取了窗口默认的标题。

ipcRenderer.invoke
是electron 7
中添加的新 api,也可以使用ipcRenderer.send
跟ipcRenderer.sendSync
来实现双向通信,但并不推荐。其原因便是开发代码的冗余跟同步代码带来的性能阻塞问题。参考
三、 主进程 ==> 渲染进程
当从主进程向渲染器进程发送消息时,需要指定哪个渲染器正在接收该消息。消息需要通过渲染进程的 WebContents
实例发送到渲染进程。此 WebContents
实例包含一个 send
方法,其使用方式与 ipcRenderer.send
相同。添加一个点击系统菜单提醒用户的功能示例:
- 在主进程使用
Menu.buildFromTemplate
添加一个 "提醒用户" 的菜单按钮,并添加send
事件。 2.preload 中使用ipcRenderer.on('message')
添加onMessage
函数暴露给渲染进程。 - renderer 中调用
window.electron.onMessage()
监听主进程发送的消息 - 还可以在 preload 中额外添加一个
sendMessage
函数来处理渲染进程需要回复消息的情况。当然,这不是必须的。
ts
# global.d.ts
export interface ElectronAPI {
onMessage: (callback: (...args: any[]) => void) => void;
replyMessage: (...args: any[]) => void;
}
declare global {
var electronAPI: ElectronAPI;
}
export {};
ts
# main.ts
import { app, BrowserWindow, ipcMain, Menu } from "electron";
import path from "node:path";
const createWindow = () => {
const win = new BrowserWindow({
width: 960,
height: 600,
webPreferences: {
nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
contextIsolation: true, // 设置是否启用上下文隔离模式。
preload: path.join(__dirname, "preload.js"),
},
});
ipcMain.on("reply-message", (_, message) => {
console.log(message); // 打印:"收到消息了😉"
});
const menu = Menu.buildFromTemplate([
{
label: "提醒用户",
click: () => {
win.webContents.send("message", "测试消息");
},
},
]);
win.setMenu(menu);
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
// win.loadFile(path.join(__dirname, '../index.html'));
// win.loadFile(path.join(__dirname, '../web-dist/index.html'));
}
};
app.whenReady().then(createWindow);
ts
# preload.ts
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("electronAPI", {
onMessage: (callback: (...args: any[]) => void) => {
ipcRenderer.on("message", (_, ...args) => callback(...args));
},
replyMessage: (message: string) => {
ipcRenderer.send("reply-message", message);
},
});
ts
# App.vue
<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");
window.electronAPI.onMessage((value) => {
window.alert(value);
window.electronAPI.replyMessage("收到消息了😉");
});
</script>
<template>
<h1>{{ title }}</h1>
</template>

四、 渲染进程 1 <==> 渲染进程 2
没有直接的方法可以使用 ipcMain
和 ipcRenderer
模块在 Electron 中的渲染进程之间发送消息。想要实现这种模式,有两种选择:
- 使用主进程作为渲染进程之间的消息代理,由渲染进程向主进程发送消息,在由主进程将消息转发到另一个渲染进程。
- 使用 MessagePort 从主进程传递到两个渲染器。这将允许在初始化后渲染器之间进行直接通信。
开始之前先做一点前置准备
- 新建 public/login.html 文件作为第二个渲染进程。
- 在 electron/main.ts 新增
createLoginWindow
函数,用于创建login
渲染进程。- 新建 electron/loginPreload.ts 文件作为
login
渲染进程的preload
脚本。- 修改 App.vue 的代码,添加一个 登录按钮,用于打开 LoginWindow。
- 在electron/main.ts 、electron/preload.ts 、App.vue 分别添加跟调用
open-login-view
这个事件的函数。- 在vite.electron.config.ts 中 添加
electron([...,{ entry: "electron/loginPreload.ts" }])
。
ts
# global.d.ts
export interface ElectronAPI {
openLoginView: () => void;
}
declare global {
var electronAPI: ElectronAPI;
}
export {};
html
# login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Login</h1>
<input type="button" value="Login" onclick="login()" />
<script>
function login() {
// 登录成功
console.log("登录成功");
}
</script>
</body>
</html>
ts
# main.ts
import { app, BrowserWindow, ipcMain } from "electron";
import path from "node:path";
const createWindow = () => {
const win = new BrowserWindow({
width: 960,
height: 600,
webPreferences: {
nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
contextIsolation: true, // 设置是否启用上下文隔离模式。
preload: path.join(__dirname, "preload.js"),
},
});
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
// win.loadFile(path.join(__dirname, '../index.html'));
// win.loadFile(path.join(__dirname, '../web-dist/index.html'));
}
};
// 🚀
const createLoginWindow = () => {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
contextIsolation: true, // 设置是否启用上下文隔离模式。
preload: path.join(__dirname, "loginPreload.js"),
},
});
if (process.env.VITE_DEV_SERVER_URL) {
// path.join(process.env.VITE_DEV_SERVER_URL, "login.html") => http://localhost:3000/login.html
win.loadURL(path.join(process.env.VITE_DEV_SERVER_URL, "login.html"));
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
}
};
// 🚀
ipcMain.handle("open-login-view", () => {
createLoginWindow();
});
app.whenReady().then(() => {
createWindow();
});
ts
# preload.ts
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("electronAPI", {
openLoginView: () => ipcRenderer.invoke("open-login-view"),
});
html
# App.vue
<script setup lang="ts">
function openLoginView() {
window.electronAPI.openLoginView();
}
</script>
<template>
<h1>首页</h1>
<button @click="openLoginView">登录</button>
</template>
ts
# vite.electron.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import electron from "vite-plugin-electron";
import { rmSync } from "node:fs";
// 先将dist、dist-electron、release文件夹强制删除在进行后续的打包流程
rmSync("dist", { recursive: true, force: true });
rmSync("dist-electron", { recursive: true, force: true });
rmSync("release", { recursive: true, force: true });
export default defineConfig({
plugins: [
vue(),
electron([
{ entry: "electron/main.ts" },
{ entry: "electron/preload.ts" },
// 🚀
{ entry: "electron/loginPreload.ts" },
]),
],
});

OK!准备就绪!下面实现一个模拟登录成功后通知主渲染进程来调用系统通知的 API 通知用户登录成功的操作。
1. 代理中转模式
这个模式我就不演示啦,流程就是
渲染进程1 <=> 主进程 <=> 渲染进程2
。
2.MessagePort 模式
这个模式是在主进程 使用 MessageChannel API 创建一个信息通道,分别给渲染进程去使用,实现两个渲染进程之间直接可以进行通信。就像一条管道一样,能够互相发送消息以及回复消息。
- 使用 MessageChannel API 创建一个消息通道,分别在
ready-to-show
事件中发送给mainWindow
跟loginWindow
两个渲染进程使用。 - 在主进程中新增名为
close-window
、show-notification
的处理器等待调用。 - 在 loginPreload.ts 、跟 preload.ts 使用
ipcRenderer.on('port')
获取MessagePort
。 - 在 loginPreload.ts 添加
postMessage
函数暴露给loginWindow
渲染进程。 - 在 login.html 调用
window.electronAPI.postMessage
通知mainWindow
已经登陆成功。 - 在 preload.ts 文件中根据
data.type
处理对应事件逻辑。
ts
# main.ts
import {
app,
BrowserWindow,
ipcMain,
MessageChannelMain,
Notification,
NotificationConstructorOptions,
} from "electron";
import path from "node:path";
// 🚀
const { port1, port2 } = new MessageChannelMain();
const createWindow = () => {
const win = new BrowserWindow({
width: 960,
height: 600,
webPreferences: {
nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
contextIsolation: true, // 设置是否启用上下文隔离模式。
preload: path.join(__dirname, "preload.js"),
},
});
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
// win.loadFile(path.join(__dirname, '../index.html'));
// win.loadFile(path.join(__dirname, '../web-dist/index.html'));
}
// 🚀
win.once("ready-to-show", () => {
win.webContents.postMessage("port", null, [port1]);
});
};
const createLoginWindow = () => {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
contextIsolation: true, // 设置是否启用上下文隔离模式。
preload: path.join(__dirname, "loginPreload.js"),
},
});
if (process.env.VITE_DEV_SERVER_URL) {
// path.join(process.env.VITE_DEV_SERVER_URL, "login.html") => http://localhost:3000/login.html
win.loadURL(path.join(process.env.VITE_DEV_SERVER_URL, "login.html"));
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
}
// 🚀
win.once("ready-to-show", () => {
win.webContents.postMessage("port", null, [port2]);
});
};
ipcMain.handle("open-login-view", () => {
createLoginWindow();
});
// 🚀
ipcMain.handle("close-window", (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.close();
});
// 🚀
ipcMain.handle(
"show-notification",
(_, options: NotificationConstructorOptions) => {
if (Notification.isSupported()) {
new Notification(options).show();
}
}
);
app.whenReady().then(createWindow);
ts
# loginPreload.ts
import { contextBridge, ipcRenderer } from "electron";
let port: MessagePort | null = null;
ipcRenderer.on("port", (event) => {
port = event.ports[0];
port.start();
});
contextBridge.exposeInMainWorld("electronAPI", {
postMessage: (message: any, options?: StructuredSerializeOptions) => {
port?.postMessage(message, options);
},
});
html
# login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Login</h1>
<input type="button" value="Login" onclick="login()" />
<script>
function login() {
// 登录成功...
// 通知主渲染进程
window.electronAPI.postMessage({
type: "login-success",
response: {
code: 200,
data: { username: "admin", password: "admin" },
message: "登录成功",
},
});
}
</script>
</body>
</html>
ts
# preload.ts
import {
contextBridge,
ipcRenderer,
type NotificationConstructorOptions,
} from "electron";
ipcRenderer.on("port", (event) => {
const port = event.ports[0];
port.onmessage = (messageEvent) => {
const { data } = messageEvent;
switch (data.type) {
case "login-success":
const options: NotificationConstructorOptions = {
title: "登录成功",
body: `您好,${data.response.data.username}。恭喜登录成功,很高心见到你!`,
};
ipcRenderer.invoke("show-notification", options);
break;
default:
console.warn("没有对应的事件处理");
break;
}
};
port.start();
});
contextBridge.exposeInMainWorld("electronAPI", {
openLoginView: () => ipcRenderer.invoke("open-login-view"),
});

这就完成啦🫡!这种模式的通信流程便是
渲染进程1 <===> 渲染进程2
,就不用像第一种模式那种需要主进程作为中间代理来实现了。
总结
在 Electron 中有 "主进程" 跟 "渲染进程" 两类进程模型。而进程的通信实现起来代码其实并不多,主要还是对进程与进程通信的概念方式的理解为主。方法就那么几个,可以灵活使用,当然最好是以官方推荐的为主。
-
主进程: 主进程负责给渲染进程提供调用系统级 API 的支持。跟管理渲染进程的创建和管理。
-
渲染进程: 渲染 Web 内容。提供了 WebApi。
-
preload : 出于安全原因,建议开启 "上下文隔离",关闭渲染模式的 "node 集成模式",并在 preload 中使用
contextBridge API
安全的暴露渲染进程需要使用的 api。 -
四种通信模式:
渲染进程 ==> 主进程
:ipcRenderer.send
/ipcRenderer.sendSync
/ipcMain.on
、ipcRenderer.invoke
/ipcMain.handle
。主进程 <==> 渲染进程
:ipcRenderer.send
/ipcRenderer.sendSync
/win.webContents.send
/ipcMain.on
/ipcRenderer.on
、ipcRenderer.invoke
/ipcMain.handle
(推荐)。主进程 ==> 渲染进程
:win.webContents.send
/ipcRenderer.on
渲染进程 <==> 渲染进程
:MessageChannel API
、主进程代理中转
这里额外补充一个最近出现的问题,就是最近刚写这篇文章的时候,重新
pnpm i
为我的项目安装依赖后,执行pnpm electron:dev
命令会报错。这时候全局安装一个依赖electron-fix
然后执行electron-fix start
来修复错误,完成以后就可以了。

任务列表
- 搭建electron项目
- 运行 electron 桌面端开发环境
- 运行 web 浏览器端开发环境
- 打包
- electron 通信