Appearance
主进程与渲染进程通信
主进程是创建窗口的进程,渲染进程是渲染页面的进程。主进程和渲染进程是两个不同的进程,它们之间不能直接通信。但是 Electron 提供了 IPC(Inter-Process Communication)模块,用于在主进程和渲染进程之间进行通信。
渲染进程是通过
ipcRenderer对象提供的方法与主进程进行通信的。主进程是使用ipcMain对象提供的方法与渲染进程进行通信的。
单向通信:ipcMain.on / ipcRenderer.send
双向通信:ipcMain.handle / ipcRenderer.invoke
安全的通信桥梁——preload.js
方法一:单向通信 send 与 on/once
这种方式非常适合“发后不理”的场景,即渲染进程向主进程发送一个消息,但不需要等待主进程的直接回复。比如,通知主进程执行某个操作(如保存文件),或者从主进程向渲染进程广播一个事件(如系统状态变更)。
流程:
- 渲染进程 (
ipcRenderer.send): 发送一个异步消息到主进程。 - 主进程 (
ipcMain.on或ipcMain.once): 监听并接收这个消息,然后执行相应的操作。
示例:渲染进程通知主进程修改窗口标题
1. preload.js:暴露发送接口
javascript
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
setTitle: (title) => ipcRenderer.send("set-title", title),
});const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
setTitle: (title) => ipcRenderer.send("set-title", title),
});2. main.js:监听并处理消息
javascript
ipcMain.on("set-title", (event, title) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
if (win) {
win.setTitle(title);
}
});ipcMain.on("set-title", (event, title) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
if (win) {
win.setTitle(title);
}
});ipcMain.on 用于持续监听一个频道。如果只想监听一次,可以使用 ipcMain.once。
回调函数中的 event 对象包含了消息的元数据,比如 event.sender 可以用来获取发送消息的 WebContents 实例。
3. renderer.js (在 index.html 中引入): 调用接口
javascript
const setButton = document.getElementById("btn-set-title");
const titleInput = document.getElementById("title");
setButton.addEventListener("click", () => {
const title = titleInput.value;
window.electronAPI.setTitle(title); // 通过 preload 暴露的 API 发送消息
});const setButton = document.getElementById("btn-set-title");
const titleInput = document.getElementById("title");
setButton.addEventListener("click", () => {
const title = titleInput.value;
window.electronAPI.setTitle(title); // 通过 preload 暴露的 API 发送消息
});方法二:双向通信 invoke 与 handle
这是 Electron 推荐的现代通信方式,特别适用于需要请求-响应模型的场景。渲染进程发送一个请求,然后异步等待主进程处理后返回一个结果。这使得代码逻辑更清晰,避免了管理多个事件回调的复杂性。
流程:
主进程 (ipcMain.handle): 设置一个消息处理器,当接收到特定频道的请求时,执行一个异步函数并返回一个 Promise。 渲染进程 (ipcRenderer.invoke): 发送一个请求到该频道,并 await 返回的 Promise 来获取结果。
示例:渲染进程请求主进程读取文件内容
1. preload.js:暴露调用接口
javascript
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
// ... 其他 API
openFile: () => ipcRenderer.invoke("dialog:openFile"),
});const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
// ... 其他 API
openFile: () => ipcRenderer.invoke("dialog:openFile"),
});2. main.js:设置处理器并返回结果
javascript
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs");
const path = require("path");
// ...
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog({});
if (!canceled) {
try {
const content = fs.readFileSync(filePaths[0], "utf-8");
return content; // 返回文件内容
} catch (error) {
console.error("Failed to read file", error);
return `Error: ${error.message}`; // 返回错误信息
}
}
return null; // 用户取消选择时返回 null
}
app.whenReady().then(() => {
ipcMain.handle("dialog:openFile", handleFileOpen);
createWindow();
});const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs");
const path = require("path");
// ...
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog({});
if (!canceled) {
try {
const content = fs.readFileSync(filePaths[0], "utf-8");
return content; // 返回文件内容
} catch (error) {
console.error("Failed to read file", error);
return `Error: ${error.message}`; // 返回错误信息
}
}
return null; // 用户取消选择时返回 null
}
app.whenReady().then(() => {
ipcMain.handle("dialog:openFile", handleFileOpen);
createWindow();
});ipcMain.handle 的回调函数必须返回一个 Promise。async 函数会隐式地返回 Promise。
函数返回的值(或 Promise resolve 的值)会被发送回渲染进程。如果抛出异常,Promise 会被 reject。
3. renderer.js: 调用接口并处理返回结果
javascript
const openButton = document.getElementById("btn-open-file");
const fileContent = document.getElementById("file-content");
openButton.addEventListener("click", async () => {
const content = await window.electronAPI.openFile();
if (content !== null) {
fileContent.textContent = content;
} else {
fileContent.textContent = "未选择文件。";
}
});const openButton = document.getElementById("btn-open-file");
const fileContent = document.getElementById("file-content");
openButton.addEventListener("click", async () => {
const content = await window.electronAPI.openFile();
if (content !== null) {
fileContent.textContent = content;
} else {
fileContent.textContent = "未选择文件。";
}
});| 特性 | send / on (单向) | invoke / handle (双向) |
|---|---|---|
| 通信模型 | 事件驱动,发布/订阅 | 请求/响应 (RPC) |
| 适用场景 | 发送通知、广播事件、触发不需要返回值的操作 | 请求数据、执行需要返回结果的异步任务 |
| 代码风格 | 基于回调和事件监听 | 基于 Promise 和 async/await,更现代、直观 |
| 推荐 | 适用于单向数据流 | 强烈推荐用于需要返回值的双向通信 |
- 当你需要从渲染进程触发一个主进程操作,并且不关心结果时,使用 send/on。例如:点击最小化按钮、记录一条日志。
- 当你需要从渲染进程请求一个数据或操作,并且需要等待其处理结果时,请务必使用 invoke/handle。例如:读取本地文件、查询数据库、获取系统信息。
小洛的前端技术博客