进程间通信
🌐 Inter-Process Communication
进程间通信(IPC)是构建功能丰富的 Electron 桌面应用的关键部分。由于在 Electron 的进程模型中,主进程和渲染进程承担不同的职责,IPC 是执行许多常见任务的唯一方式,例如从用户界面调用本地 API 或从本地菜单触发网页内容的更改。
🌐 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 中,进程通过开发者定义的“通道”使用 ipcMain 和 ipcRenderer 模块传递消息进行通信。这些通道是任意的(你可以随意命名)并且是双向的(两个模块可以使用相同的通道名称)。
🌐 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.
模式 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.
你通常使用这个模式从你的网页内容调用主进程 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.
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
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()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/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>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
1. 监听 ipcMain.on 事件
🌐 1. 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:
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 回调有两个参数:一个 IpcMain事件 结构和一个 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.html 和 preload.js 入口点!
🌐 Make sure you're loading the index.html and preload.js entry points for the following steps!
2. 通过预加载暴露 ipcRenderer.send
🌐 2. 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.
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. 构建渲染进程的用户界面
🌐 3. 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:
<!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 文件中添加几行代码,这些代码利用了 preload 脚本中公开的 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:
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
到此为止,你的演示应该已经可以完全运行了。试着使用输入框,看看会对你的浏览器窗口标题产生什么影响!
🌐 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.invoke 配合 ipcMain.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.
- main.js
- preload.js
- index.html
- renderer.js
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()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/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>
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
1. 监听 ipcMain.handle 事件
🌐 1. Listen for events with ipcMain.handle
在主进程中,我们将创建一个 handleFileOpen() 函数,该函数调用 dialog.showOpenDialog 并返回用户选择的文件路径的值。每当从渲染进程通过 dialog:openFile 通道发送 ipcRenderer.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 ipcRenderer.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.
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.html 和 preload.js 入口点!
🌐 Make sure you're loading the index.html and preload.js entry points for the following steps!
2. 通过预加载暴露 ipcRenderer.invoke
🌐 2. 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.
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. 构建渲染进程的用户界面
🌐 3. Build the renderer process UI
最后,让我们构建加载到浏览器窗口中的 HTML 文件。
🌐 Finally, let's build the HTML file that we load into our BrowserWindow.
<!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>
用户界面由一个 #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:
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
ipcRenderer.invoke API 在 Electron 7 中被加入,作为一种开发者友好的方式来处理来自渲染进程的双向 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.
// 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')
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,你可以将响应值作为 Promise 返回给原来的 API 调用。 - 没有明显的方法将
asynchronous-reply消息与原始的asynchronous-message消息对应起来。如果这些通道中来回传输的消息非常频繁,你需要添加额外的应用代码来单独跟踪每个调用和响应。
使用 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.
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
// 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.
- main.js
- preload.js
- index.html
- renderer.js
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()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/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>
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)
})
1. 使用 webContents 模块发送消息
🌐 1. 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.
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.html 和 preload.js 入口点!
🌐 Make sure you're loading the index.html and preload.js entry points for the following steps!
2. 通过预加载暴露 ipcRenderer.on
🌐 2. Expose ipcRenderer.on via preload
像在前一个从渲染器到主进程的示例中一样,我们在预加载脚本中使用 contextBridge 和 ipcRenderer 模块,将 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:
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 invokes 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.
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. 构建渲染进程的用户界面
🌐 3. 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:
<!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.
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.
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)
})
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.
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...
模式 4:渲染器到渲染器
🌐 Pattern 4: Renderer to renderer
在 Electron 中,无法直接使用 ipcMain 和 ipcRenderer 模块在渲染器进程之间发送消息。要实现这一点,你有两个选项:
🌐 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:
- 使用主进程作为渲染器之间的消息代理。这将涉及从一个渲染器向主进程发送消息,然后主进程将消息转发给另一个渲染器。
- 从主进程向两个渲染器传递 消息端口。这将允许在初始设置之后渲染器之间直接通信。
对象序列化
🌐 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 对象(例如 Element、Location 和 DOMMatrix)、由 C++ 类支持的 Node.js 对象(例如 process.env,Stream 的一些成员)以及由 C++ 类支持的 Electron 对象(例如 WebContents、BrowserWindow 和 WebFrame)不能通过结构化克隆进行序列化。
🌐 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.