Skip to content

主进程与渲染进程通信

主进程是创建窗口的进程,渲染进程是渲染页面的进程。主进程和渲染进程是两个不同的进程,它们之间不能直接通信。但是 Electron 提供了 IPC(Inter-Process Communication)模块,用于在主进程和渲染进程之间进行通信。

渲染进程是通过 ipcRenderer 对象提供的方法与主进程进行通信的。主进程是使用 ipcMain 对象提供的方法与渲染进程进行通信的。

单向通信:ipcMain.on / ipcRenderer.send
双向通信:ipcMain.handle / ipcRenderer.invoke

安全的通信桥梁——preload.js

方法一:单向通信 sendon/once

这种方式非常适合“发后不理”的场景,即渲染进程向主进程发送一个消息,但不需要等待主进程的直接回复。比如,通知主进程执行某个操作(如保存文件),或者从主进程向渲染进程广播一个事件(如系统状态变更)。

流程:

  1. 渲染进程 (ipcRenderer.send): 发送一个异步消息到主进程。
  2. 主进程 (ipcMain.onipcMain.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。例如:读取本地文件、查询数据库、获取系统信息。

程序员小洛文档