进程间通信
¥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" 与 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.
-
有关 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.
- main.js
- preload.js
- index.html
- renderer.js
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()
})
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
监听事件
¥ 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
回调有两个参数: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.html
和 preload.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.
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:
<!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:
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.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
监听事件
¥ 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.
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
¥ 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. 构建渲染器进程 UI
¥ 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>
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:
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.
// 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
,你可以获得作为原始 API 调用的 Promise 返回的响应值。¥You need to set up a second
ipcRenderer.on
listener to handle the response in the renderer process. Withinvoke
, 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 originalasynchronous-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.
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
模块发送消息
¥ 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
¥ 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 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.
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:
<!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
没有直接的方法可以使用 ipcMain
和 ipcRenderer
模块在 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 对象(例如 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.