Skip to main content

进程间通信

¥Inter-Process Communication

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

¥Inter-process communication (IPC) is a key part of building feature-rich desktop applications in Electron. Because the main and renderer processes have different responsibilities in Electron's process model, IPC is the only way to perform many common tasks, such as calling a native API from your UI or triggering changes in your web contents from native menus.

工控通道

¥IPC channels

在 Electron 中,进程通过开发者定义的 "channels" 与 ipcMainipcRenderer 模块传递消息来进行通信。这些通道是任意的(你可以将它们命名为任何你想要的名称)和双向的(你可以为两个模块使用相同的通道名称)。

¥In Electron, processes communicate by passing messages through developer-defined "channels" with the ipcMain and ipcRenderer modules. These channels are arbitrary (you can name them anything you want) and bidirectional (you can use the same channel name for both modules).

在本指南中,我们将通过具体示例介绍一些基本的 IPC 模式,你可以将其用作应用代码的参考。

¥In this guide, we'll be going over some fundamental IPC patterns with concrete examples that you can use as a reference for your app code.

了解上下文隔离的进程

¥Understanding context-isolated processes

在继续了解实现细节之前,你应该熟悉使用 预加载脚本 在上下文隔离的渲染器进程中导入 Node.js 和 Electron 模块的想法。

¥Before proceeding to implementation details, you should be familiar with the idea of using a preload script to import Node.js and Electron modules in a context-isolated renderer process.

  • 有关 Electron 进程模型的完整概述,你可以阅读 进程模型文档

    ¥For a full overview of Electron's process model, you can read the process model docs.

  • 有关使用 contextBridge 模块从预加载脚本公开 API 的入门知识,请查看 上下文隔离教程

    ¥For a primer into exposing APIs from your preload script using the contextBridge module, check out the context isolation tutorial.

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

¥Pattern 1: Renderer to main (one-way)

要从渲染器进程向主进程触发单向 IPC 消息,你可以使用 ipcRenderer.send API 发送一条消息,然后由 ipcMain.on API 接收该消息。

¥To fire a one-way IPC message from a renderer process to the main process, you can use the ipcRenderer.send API to send a message that is then received by the ipcMain.on API.

你通常使用此模式从 Web 内容调用主进程 API。我们将通过创建一个可以以编程方式更改其窗口标题的简单应用来演示此模式。

¥You usually use this pattern to call a main process API from your web contents. We'll demonstrate this pattern by creating a simple app that can programmatically change its window title.

对于此演示,你需要将代码添加到主进程、渲染器进程和预加载脚本中。完整的代码如下,但我们将在以下部分中单独解释每个文件。

¥For this demo, you'll need to add code to your main process, your renderer process, and a preload script. The full code is below, but we'll be explaining each file individually in the following sections.

const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

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

ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})

mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

1. 使用 ipcMain.on 监听事件

¥ Listen for events with ipcMain.on

在主进程中,使用 ipcMain.on API 在 set-title 通道上设置 IPC 监听器:

¥In the main process, set an IPC listener on the set-title channel with the ipcMain.on API:

main.js (Main Process)
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')

// ...

function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}

function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle)
createWindow()
})
// ...

上面的 handleSetTitle 回调有两个参数:Ipc 主事件 结构和 title 字符串。每当消息通过 set-title 通道时,此函数都会找到附加到消息发送者的 BrowserWindow 实例,并在其上使用 win.setTitle API。

¥The above handleSetTitle callback has two parameters: an IpcMainEvent structure and a title string. Whenever a message comes through the set-title channel, this function will find the BrowserWindow instance attached to the message sender and use the win.setTitle API on it.

信息

确保你正在加载以下步骤的 index.htmlpreload.js 入口点!

¥Make sure you're loading the index.html and preload.js entry points for the following steps!

2. 通过预加载暴露 ipcRenderer.send

¥ Expose ipcRenderer.send via preload

要将消息发送到上面创建的监听器,你可以使用 ipcRenderer.send API。默认情况下,渲染器进程没有 Node.js 或 Electron 模块访问权限。作为应用开发者,你需要使用 contextBridge API 选择要从预加载脚本中公开哪些 API。

¥To send messages to the listener created above, you can use the ipcRenderer.send API. By default, the renderer process has no Node.js or Electron module access. As an app developer, you need to choose which APIs to expose from your preload script using the contextBridge API.

在预加载脚本中,添加以下代码,这将向渲染器进程公开全局 window.electronAPI 变量。

¥In your preload script, add the following code, which will expose a global window.electronAPI variable to your renderer process.

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})

此时,你将能够在渲染器进程中使用 window.electronAPI.setTitle() 函数。

¥At this point, you'll be able to use the window.electronAPI.setTitle() function in the renderer process.

安全警告

我们不会直接为 安全原因 公开整个 ipcRenderer.send API。确保尽可能限制渲染器对 Electron API 的访问。

¥We don't directly expose the whole ipcRenderer.send API for security reasons. Make sure to limit the renderer's access to Electron APIs as much as possible.

3. 构建渲染器进程 UI

¥ Build the renderer process UI

在我们的 BrowserWindow 加载的 HTML 文件中,添加一个由文本输入和按钮组成的基本用户界面:

¥In our BrowserWindow's loaded HTML file, add a basic user interface consisting of a text input and a button:

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://web.nodejs.cn/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>

为了使这些元素具有交互性,我们将在导入的 renderer.js 文件中添加几行代码,以利用预加载脚本中公开的 window.electronAPI 功能:

¥To make these elements interactive, we'll be adding a few lines of code in the imported renderer.js file that leverages the window.electronAPI functionality exposed from the preload script:

renderer.js (Renderer Process)
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})

此时,你的演示应该功能齐全。尝试使用输入字段,看看你的 BrowserWindow 标题会发生什么!

¥At this point, your demo should be fully functional. Try using the input field and see what happens to your BrowserWindow title!

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

¥Pattern 2: Renderer to main (two-way)

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

¥A common application for two-way IPC is calling a main process module from your renderer process code and waiting for a result. This can be done by using ipcRenderer.invoke paired with ipcMain.handle.

在下面的示例中,我们将从渲染器进程打开一个原生文件对话框并返回所选文件的路径。

¥In the following example, we'll be opening a native file dialog from the renderer process and returning the selected file's path.

对于此演示,你需要将代码添加到主进程、渲染器进程和预加载脚本中。完整的代码如下,但我们将在以下部分中单独解释每个文件。

¥For this demo, you'll need to add code to your main process, your renderer process, and a preload script. The full code is below, but we'll be explaining each file individually in the following sections.

const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')

async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
}

function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

1. 使用 ipcMain.handle 监听事件

¥ Listen for events with ipcMain.handle

在主进程中,我们将创建一个 handleFileOpen() 函数,该函数调用 dialog.showOpenDialog 并返回用户选择的文件路径的值。每当渲染器进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,此函数将用作回调。然后返回值作为 Promise 返回到原始 invoke 调用。

¥In the main process, we'll be creating a handleFileOpen() function that calls dialog.showOpenDialog and returns the value of the file path selected by the user. This function is used as a callback whenever an ipcRender.invoke message is sent through the dialog:openFile channel from the renderer process. The return value is then returned as a Promise to the original invoke call.

关于错误处理的一句话

主进程中通过 handle 抛出的错误并不透明,因为它们是序列化的,并且仅将原始错误中的 message 属性提供给渲染器进程。详情请参阅 #24427

¥Errors thrown through handle in the main process are not transparent as they are serialized and only the message property from the original error is provided to the renderer process. Please refer to #24427 for details.

main.js (Main Process)
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('node:path')

// ...

async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
}
}

function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
// ...
在通道名称上

IPC 通道名称中的 dialog: 前缀对代码没有影响。它仅充当有助于提高代码可读性的命名空间。

¥The dialog: prefix on the IPC channel name has no effect on the code. It only serves as a namespace that helps with code readability.

信息

确保你正在加载以下步骤的 index.htmlpreload.js 入口点!

¥Make sure you're loading the index.html and preload.js entry points for the following steps!

2. 通过预加载暴露 ipcRenderer.invoke

¥ Expose ipcRenderer.invoke via preload

在预加载脚本中,我们公开了一个单行 openFile 函数,该函数调用并返回 ipcRenderer.invoke('dialog:openFile') 的值。我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。

¥In the preload script, we expose a one-line openFile function that calls and returns the value of ipcRenderer.invoke('dialog:openFile'). We'll be using this API in the next step to call the native dialog from our renderer's user interface.

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
安全警告

我们不会直接为 安全原因 公开整个 ipcRenderer.invoke API。确保尽可能限制渲染器对 Electron API 的访问。

¥We don't directly expose the whole ipcRenderer.invoke API for security reasons. Make sure to limit the renderer's access to Electron APIs as much as possible.

3. 构建渲染器进程 UI

¥ Build the renderer process UI

最后,让我们构建加载到浏览器窗口中的 HTML 文件。

¥Finally, let's build the HTML file that we load into our BrowserWindow.

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://web.nodejs.cn/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer.js'></script>
</body>
</html>

UI 由一个 #btn 按钮元素(将用于触发我们的预加载 API)和一个 #filePath 元素(将用于显示所选文件的路径)组成。要使这些部分正常工作,需要在渲染器处理脚本中添加几行代码:

¥The UI consists of a single #btn button element that will be used to trigger our preload API, and a #filePath element that will be used to display the path of the selected file. Making these pieces work will take a few lines of code in the renderer process script:

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
})

在上面的代码片段中,我们监听 #btn 按钮的点击,并调用 window.electronAPI.openFile() API 来激活原生“打开文件”对话框。然后我们在 #filePath 元素中显示选定的文件路径。

¥In the above snippet, we listen for clicks on the #btn button, and call our window.electronAPI.openFile() API to activate the native Open File dialog. We then display the selected file path in the #filePath element.

注意:旧方法

¥Note: legacy approaches

Electron 7 中添加了 ipcRenderer.invoke API,作为一种开发者友好的方式来处理渲染器进程中的双向 IPC。然而,存在几种针对此 IPC 模式的替代方法。

¥The ipcRenderer.invoke API was added in Electron 7 as a developer-friendly way to tackle two-way IPC from the renderer process. However, a couple of alternative approaches to this IPC pattern exist.

如果可能的话,避免旧版方法

我们建议尽可能使用 ipcRenderer.invoke。为了历史目的,记录了以下双向渲染器到主模式。

¥We recommend using ipcRenderer.invoke whenever possible. The following two-way renderer-to-main patterns are documented for historical purposes.

信息

对于以下示例,我们直接从预加载脚本调用 ipcRenderer 以保持代码示例较小。

¥For the following examples, we're calling ipcRenderer directly from the preload script to keep the code samples small.

使用 ipcRenderer.send

¥Using ipcRenderer.send

我们用于单向通信的 ipcRenderer.send API 也可以用于执行双向通信。这是 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。

¥The ipcRenderer.send API that we used for single-way communication can also be leveraged to perform two-way communication. This was the recommended way for asynchronous two-way communication via IPC prior to Electron 7.

preload.js (Preload Script)
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // prints "pong" in the DevTools console
})
ipcRenderer.send('asynchronous-message', 'ping')
main.js (Main Process)
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
// works like `send`, but returning a message back
// to the renderer that sent the original message
event.reply('asynchronous-reply', 'pong')
})

这种方法有几个缺点:

¥There are a couple downsides to this approach:

  • 你需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。使用 invoke,你可以获得作为原始 API 调用的 Promise 返回的响应值。

    ¥You need to set up a second ipcRenderer.on listener to handle the response in the renderer process. With invoke, you get the response value returned as a Promise to the original API call.

  • 没有明显的方法将 asynchronous-reply 消息与原始 asynchronous-message 消息配对。如果你通过这些渠道来回发送非常频繁的消息,则需要添加额外的应用代码来单独跟踪每个调用和响应。

    ¥There's no obvious way to pair the asynchronous-reply message to the original asynchronous-message one. If you have very frequent messages going back and forth through these channels, you would need to add additional app code to track each call and response individually.

使用 ipcRenderer.sendSync

¥Using ipcRenderer.sendSync

ipcRenderer.sendSync API 向主进程发送消息并同步等待响应。

¥The ipcRenderer.sendSync API sends a message to the main process and waits synchronously for a response.

main.js (Main Process)
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
preload.js (Preload Script)
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')

const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // prints "pong" in the DevTools console

此代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。其同步性质意味着它将阻塞渲染器进程,直到收到响应。

¥The structure of this code is very similar to the invoke model, but we recommend avoiding this API for performance reasons. Its synchronous nature means that it'll block the renderer process until a reply is received.

模式 3:主要到渲染器

¥Pattern 3: Main to renderer

当从主进程向渲染器进程发送消息时,需要指定哪个渲染器正在接收该消息。消息需要通过渲染器进程的 WebContents 实例发送到渲染器进程。此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

¥When sending a message from the main process to a renderer process, you need to specify which renderer is receiving the message. Messages need to be sent to a renderer process via its WebContents instance. This WebContents instance contains a send method that can be used in the same way as ipcRenderer.send.

为了演示这种模式,我们将构建一个由原生操作系统菜单控制的数字计数器。

¥To demonstrate this pattern, we'll be building a number counter controlled by the native operating system menu.

对于此演示,你需要将代码添加到主进程、渲染器进程和预加载脚本中。完整的代码如下,但我们将在以下部分中单独解释每个文件。

¥For this demo, you'll need to add code to your main process, your renderer process, and a preload script. The full code is below, but we'll be explaining each file individually in the following sections.

const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node: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)
mainWindow.loadFile('index.html')

// Open the DevTools.
mainWindow.webContents.openDevTools()
}

app.whenReady().then(() => {
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

1. 使用 webContents 模块发送消息

¥ Send messages with the webContents module

对于这个演示,我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。

¥For this demo, we'll need to first build a custom menu in the main process using Electron's Menu module that uses the webContents.send API to send an IPC message from the main process to the target renderer.

main.js (Main Process)
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node: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)

mainWindow.loadFile('index.html')
}
// ...

出于本教程的目的,请务必注意 click 处理程序通过 update-counter 通道向渲染器进程发送消息(1-1)。

¥For the purposes of the tutorial, it's important to note that the click handler sends a message (either 1 or -1) to the renderer process through the update-counter channel.

click: () => mainWindow.webContents.send('update-counter', -1)
信息

确保你正在加载以下步骤的 index.htmlpreload.js 入口点!

¥Make sure you're loading the index.html and preload.js entry points for the following steps!

2. 通过预加载暴露 ipcRenderer.on

¥ Expose ipcRenderer.on via preload

与前面的渲染器到主示例一样,我们在预加载脚本中使用 contextBridgeipcRenderer 模块来向渲染器进程公开 IPC 功能:

¥Like in the previous renderer-to-main example, we use the contextBridge and ipcRenderer modules in the preload script to expose IPC functionality to the renderer process:

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})

加载预加载脚本后,你的渲染器进程应该可以访问 window.electronAPI.onUpdateCounter() 监听器函数。

¥After loading the preload script, your renderer process should have access to the window.electronAPI.onUpdateCounter() listener function.

安全警告

我们不会直接为 安全原因 公开整个 ipcRenderer.on API。确保尽可能限制渲染器对 Electron API 的访问。另外,不要只将回调传递给 ipcRenderer.on,因为这会通过 event.sender 泄漏 ipcRenderer。使用仅使用所需参数调用 callback 的自定义处理程序。

¥We don't directly expose the whole ipcRenderer.on API for security reasons. Make sure to limit the renderer's access to Electron APIs as much as possible. Also don't just pass the callback to ipcRenderer.on as this will leak ipcRenderer via event.sender. Use a custom handler that invoke the callback only with the desired arguments.

信息

在这个最小示例的情况下,你可以直接在预加载脚本中调用 ipcRenderer.on,而不是通过上下文桥公开它。

¥In the case of this minimal example, you can call ipcRenderer.on directly in the preload script rather than exposing it over the context bridge.

preload.js (Preload Script)
const { ipcRenderer } = require('electron')

window.addEventListener('DOMContentLoaded', () => {
const counter = document.getElementById('counter')
ipcRenderer.on('update-counter', (_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
})
})

然而,与通过上下文桥公开预加载 API 相比,这种方法的灵活性有限,因为你的监听器无法直接与渲染器代码交互。

¥However, this approach has limited flexibility compared to exposing your preload APIs over the context bridge, since your listener can't directly interact with your renderer code.

3. 构建渲染器进程 UI

¥ Build the renderer process UI

为了将它们结合在一起,我们将在加载的 HTML 文件中创建一个界面,其中包含一个 #counter 元素,我们将用它来显示值:

¥To tie it all together, we'll create an interface in the loaded HTML file that contains a #counter element that we'll use to display the values:

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://web.nodejs.cn/en-US/docs/Web/HTTP/CSP -->
<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>

最后,为了使 HTML 文档中的值更新,我们将添加几行 DOM 操作,以便每当我们触发 update-counter 事件时都会更新 #counter 元素的值。

¥Finally, to make the values update in the HTML document, we'll add a few lines of DOM manipulation so that the value of the #counter element is updated whenever we fire an update-counter event.

renderer.js (Renderer Process)
const counter = document.getElementById('counter')

window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})

在上面的代码中,我们将回调传递给从预加载脚本公开的 window.electronAPI.onUpdateCounter 函数。第二个 value 参数对应于我们从原生菜单的 webContents.send 调用中传入的 1-1

¥In the above code, we're passing in a callback to the window.electronAPI.onUpdateCounter function exposed from our preload script. The second value parameter corresponds to the 1 or -1 we were passing in from the webContents.send call from the native menu.

可选的:返回回复

¥Optional: returning a reply

对于主到渲染器 IPC,没有 ipcRenderer.invoke 的等效项。相反,你可以从 ipcRenderer.on 回调中将响应发送回主进程。

¥There's no equivalent for ipcRenderer.invoke for main-to-renderer IPC. Instead, you can send a reply back to the main process from within the ipcRenderer.on callback.

我们可以通过对前面示例中的代码进行轻微修改来演示这一点。在渲染器进程中,公开另一个 API 以通过 counter-value 通道将响应发送回主进程。

¥We can demonstrate this with slight modifications to the code from the previous example. In the renderer process, expose another API to send a reply back to the main process through the counter-value channel.

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
renderer.js (Renderer Process)
const counter = document.getElementById('counter')

window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})

在主进程中,监听 counter-value 事件并进行适当的处理。

¥In the main process, listen for counter-value events and handle them appropriately.

main.js (Main Process)
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...

模式 4:渲染器到渲染器

¥Pattern 4: Renderer to renderer

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。为了实现这一目标,你有两种选择:

¥There's no direct way to send messages between renderer processes in Electron using the ipcMain and ipcRenderer modules. To achieve this, you have two options:

  • 使用主进程作为渲染器之间的消息代理。这将涉及从一个渲染器向主进程发送消息,主进程会将消息转发到另一个渲染器。

    ¥Use the main process as a message broker between renderers. This would involve sending a message from one renderer to the main process, which would forward the message to the other renderer.

  • MessagePort 从主进程传递到两个渲染器。这将允许在初始设置后渲染器之间进行直接通信。

    ¥Pass a MessagePort from the main process to both renderers. This will allow direct communication between renderers after the initial setup.

对象序列化

¥Object serialization

Electron 的 IPC 实现使用 HTML 标准 结构化克隆算法 来序列化进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。

¥Electron's IPC implementation uses the HTML standard Structured Clone Algorithm to serialize objects passed between processes, meaning that only certain types of objects can be passed through IPC channels.

特别是,DOM 对象(例如 ElementLocationDOMMatrix)、C++ 类支持的 Node.js 对象(例如 process.envStream 的某些成员)和 C++ 类支持的 Electron 对象(例如 WebContentsBrowserWindowWebFrame)是不可序列化的 与结构化克隆。

¥In particular, DOM objects (e.g. Element, Location and DOMMatrix), Node.js objects backed by C++ classes (e.g. process.env, some members of Stream), and Electron objects backed by C++ classes (e.g. WebContents, BrowserWindow and WebFrame) are not serializable with Structured Clone.