跳到主要内容

进程间通信

进程间通信(IPC)是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

主进程的 ipcMain 和 渲染进程的 ipcRenderer 都是 EventEmitter 对象。

在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。

这些通道是 任意(您可以随意命名它们)和 双向(您可以在两个模块中使用相同的通道名称)的。在主进程中使用 ipcMain,在渲染进程中使用 ipcRenderer。

IPC 通道名称上的 :前缀 对代码没有影响,比如:ipcMain.handle('dialog:openFile', handleFileOpen) 中的 dialog:。它仅用作命名空间以帮助提高代码的可读性。

模式 1:渲染器进程到主进程(单向)

要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send (一般是封装在 preload.js)API 发送消息,然后主进程中使用 ipcMain.on(比如,main.js) API 接收。

模式 2:渲染器进程到主进程(双向)

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invokeipcMain.handle 搭配使用来完成。

  • 一般是在主进程的 ready 后使用 ipcMain.handle(typeName, eventCallback)
  • 通过预加载脚本暴露 ipcRenderer.invoke
// main.js (Main Process)
app.whenReady(() => {
ipcMain.handle("dialog:openFile", handleFileOpen);
createWindow();
});
// preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
openFile: () => ipcRenderer.invoke("dialog:openFile"),
});
// renderer.js (Renderer Process)
const btn = document.getElementById("btn");
const filePathElement = document.getElementById("filePath");

btn.addEventListener("click", async () => {
const filePath = await window.electronAPI.openFile();
filePathElement.innerText = filePath;
});

模式 3:主进程到渲染器进程

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。主进程消息需要通过其 WebContents 实例发送到渲染器进程。此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。然后通过预加载脚本暴露 ipcRenderer.on 监听事件。

// main.js (Main Process)
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
const path = require("path");

function createWindow() {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});

const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send("update-counter", 1),
label: "Increment",
},
{
click: () => mainWindow.webContents.send("update-counter", -1),
label: "Decrement",
},
],
},
]);
Menu.setApplicationMenu(menu);

// 监听渲染进程的回调,key 为 counter-value
ipcMain.on("counter-value", (event, value) => {
console.log(value); // 将打印到 Node 控制台
});

mainWindow.loadFile("index.html");
}
//...
// preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
onUpdateCounter: (callback) => ipcRenderer.on("update-counter", callback),
});

加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.onUpdateCounter() 监听器函数。

出于安全原因,我们不会直接暴露整个 ipcRenderer.on API。确保尽可能限制渲染器对 Electron API 的访问。

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>
// renderer.js (Renderer Process)
const counter = document.getElementById("counter");

window.electronAPI.onUpdateCounter((event, value) => {
const oldValue = Number(counter.innerText);
const newValue = oldValue + value;
counter.innerText = newValue;
// 向主进程方式事件及参数,可以为 counter-value,参数为 newValue
event.sender.send("counter-value", newValue);
});

模式 4:渲染器进程到渲染器进程

没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。为此,您有两种选择:

  • 将主进程作为渲染器之间的消息代理。这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 从主进程将一个 MessagePort 传递到两个渲染器。这将允许在初始设置后渲染器之间直接进行通信。
ipcRenderer.sendTo(webContentsId, channel, ...args);
ipcRenderer.sendToHost(channel, ...args);

小结

  1. 主进程(ipcMain)和渲染器进程(ipcRenderer)之间的通信主要是走这两种模式:
  • ipcRenderer.send <=> ipcMain.on
ipcMain.on("from-browser-msg", async (event, args) => {
console.log("ipcMain.on from-browser-msg", args);
event.reply(
"reply-from-browser-msg",
"ipcMain.on event.reply,哈哈,我收到了,来自主进程回复"
);
});
window.ipcRenderer.send("from-browser-msg", {
msg: "hello, from-browser-msg!",
});

window.ipcRenderer.on("reply-from-browser-msg", (event, data) => {
console.log("Browser 接受来自主进程 reply-from-browser-msg 的信息", data);
});
  • ipcRenderer.invoke <=> ipcMain.handle
ipcMain.handle("from-browser-msg", async (event, args) => {
console.log("ipcMain.handle from-browser-msg", args);
event.sender.send(
"reply-from-browser-msg",
"ipcMain.handle,event.sender.send,哈哈,我收到了,来自主进程"
);
return "return的值,then可以接收到";
});
// invoke 返回的是一个promise对象,内容为 handle 中 return 返回的值。
window.ipcRenderer
.invoke("from-browser-msg", { msg: "hello, from-browser-msg!" })
.then((res) => {
console.log("ipcRenderer.invoke then", res);
});
  1. 渲染器和渲染器通讯

目前是采用第一种的方案,即通过主进程中转方式。简单的说,就是在主进程设置一个代理转发的事件,然后分发给所有渲染进程,渲染进程根据 channel 名称拿到事件及传参。示例代码如下:

// 主进程监听 broadcast,然后转发给所有渲染进程
ipcMain.on("broadcast", (_event, channel = "broadcast", ...rest) => {
// 获取所有渲染进程,然后分别分发
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send(channel, ...rest);
window?.getBrowserViews()?.forEach((view) => {
view?.webContents?.send(channel, ...rest);
});
});
});
// 渲染进程发送 broadcast
ipcRenderer.send("broadcast", channel, ...args);
  1. 渲染器和渲染器中 webview 通讯
// ipcRenderer.sendToHost(channel, ...arg);
window.ipcRenderer.sendToHost("from-browser-msg-host", {
msg: "hello, sendToHostMsg, from-browser-msg!",
});
const handleIpcEvent = ({ channel, args, target }) => {
// todo
};

// webview 中指定且只能通过 ipc-message 接受事件
webview.addEventListener("ipc-message", handleIpcEvent);
// 组件销毁记得要 webview.removeEventListener('ipc-message', handleIpcEvent);
  • ipcRenderer.send 发往主进程
  • ipcRenderer.sendTo、ipcRenderer.sendToHost 发往渲染进程,其中 ipcRenderer.sendToHost 是发往渲染进程中的 webview