进程间通信
进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分。由于主进程和渲染进程在 Electron 的进程模型中承担着不同的职责,因此 IPC 是执行许多常见任务的唯一方式,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容中的更改。
IPC 通道
在 Electron 中,进程通过使用 ipcMain
和 ipcRenderer
模块,通过开发者定义的“通道”传递消息来进行通信。这些通道是任意的(您可以随意命名)并且是双向的(您可以对这两个模块使用相同的通道名称)。
在本指南中,我们将介绍一些基本的 IPC 模式,并提供具体的示例,您可以将其用作应用程序代码的参考。
了解上下文隔离进程
在继续进行实现细节之前,您应该熟悉使用预加载脚本在上下文隔离的渲染进程中导入 Node.js 和 Electron 模块的概念。
模式 1:渲染器到主进程(单向)
要从渲染进程向主进程发送单向 IPC 消息,您可以使用 ipcRenderer.send
API 发送消息,然后由 ipcMain.on
API 接收。
您通常使用此模式从 Web 内容调用主进程 API。我们将通过创建一个可以以编程方式更改其窗口标题的简单应用程序来演示此模式。
对于此演示,您需要向主进程、渲染进程和预加载脚本添加代码。完整的代码如下所示,但我们将在以下部分中分别解释每个文件。
- 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://mdn.org.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>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
1. 使用 ipcMain.on
监听事件
在主进程中,使用 ipcMain.on
API 在 set-title
通道上设置 IPC 监听器
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
回调有两个参数:一个IpcMainEvent 结构和一个 title
字符串。每当通过 set-title
通道收到消息时,此函数都会找到附加到消息发送者的 BrowserWindow 实例,并在其上使用 win.setTitle
API。
确保您正在加载 index.html
和 preload.js
入口点以进行以下步骤!
2. 通过预加载公开 ipcRenderer.send
要发送消息到上面创建的监听器,您可以使用 ipcRenderer.send
API。默认情况下,渲染进程没有 Node.js 或 Electron 模块访问权限。作为应用程序开发者,您需要使用 contextBridge
API 选择要从预加载脚本公开哪些 API。
在您的预加载脚本中,添加以下代码,它将向您的渲染进程公开一个全局 window.electronAPI
变量。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
此时,您将能够在渲染进程中使用 window.electronAPI.setTitle()
函数。
出于安全原因,我们不会直接公开整个 ipcRenderer.send
API。请确保尽可能限制渲染器对 Electron API 的访问。
3. 构建渲染进程 UI
在我们的 BrowserWindow 加载的 HTML 文件中,添加一个由文本输入和按钮组成的基本用户界面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://mdn.org.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
功能
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
此时,您的演示应该可以完全正常运行。尝试使用输入字段,看看您的 BrowserWindow 标题发生了什么变化!
模式 2:渲染器到主进程(双向)
双向 IPC 的一个常见应用是从渲染进程代码调用主进程模块并等待结果。这可以通过使用 ipcRenderer.invoke
与 ipcMain.handle
配合来完成。
在下面的示例中,我们将从渲染进程打开一个本机文件对话框并返回所选文件的路径。
对于此演示,您需要向主进程、渲染进程和预加载脚本添加代码。完整的代码如下所示,但我们将在以下部分中分别解释每个文件。
- 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://mdn.org.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>
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
监听事件
在主进程中,我们将创建一个 handleFileOpen()
函数,该函数调用 dialog.showOpenDialog
并返回用户选择的“文件路径”的值。每当从渲染进程通过 dialog:openFile
通道发送 ipcRender.invoke
消息时,此函数将用作回调。返回值将作为 Promise 返回到原始的 invoke
调用。
在主进程中通过 handle
抛出的错误不是透明的,因为它们被序列化,并且只有原始错误的 message
属性提供给渲染进程。有关详细信息,请参阅#24427。
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:
前缀对代码没有影响。它仅用作有助于代码可读性的命名空间。
确保您正在加载 index.html
和 preload.js
入口点以进行以下步骤!
2. 通过预加载公开 ipcRenderer.invoke
在预加载脚本中,我们公开了一个单行 openFile
函数,该函数调用并返回 ipcRenderer.invoke('dialog:openFile')
的值。我们将在下一步中使用此 API 从渲染器的用户界面调用本机对话框。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
出于安全原因,我们不会直接公开整个 ipcRenderer.invoke
API。请确保尽可能限制渲染器对 Electron API 的访问。
3. 构建渲染进程 UI
最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://mdn.org.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 由一个用于触发预加载 API 的单个 #btn
按钮元素和一个用于显示所选文件路径的 #filePath
元素组成。使这些部分工作需要在渲染进程脚本中添加几行代码
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
元素中显示所选的文件路径。
注意:传统方法
ipcRenderer.invoke
API 从 Electron 7 开始引入,旨在为开发者提供一种用户友好的方式来处理渲染进程中的双向 IPC。但是,也存在一些针对这种 IPC 模式的替代方法。
我们建议尽可能使用 ipcRenderer.invoke
。以下渲染进程到主进程的双向模式出于历史目的而被记录在案。
在以下示例中,我们直接从预加载脚本调用 ipcRenderer
以使代码示例保持简洁。
使用 ipcRenderer.send
我们用于单向通信的 ipcRenderer.send
API 也可以用于执行双向通信。在 Electron 7 之前,这是通过 IPC 进行异步双向通信的推荐方法。
// 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')
})
这种方法有几个缺点
- 您需要设置第二个
ipcRenderer.on
监听器来处理渲染进程中的响应。使用invoke
,您可以将响应值作为 Promise 返回到原始 API 调用。 - 没有明显的方法将
asynchronous-reply
消息与原始asynchronous-message
消息配对。如果这些通道之间频繁地来回传递消息,则需要添加额外的应用程序代码来单独跟踪每个调用和响应。
使用 ipcRenderer.sendSync
ipcRenderer.sendSync
API 将消息发送到主进程,并同步等待响应。
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**。它的同步特性意味着它会阻塞渲染进程,直到收到回复。
模式 3:主进程到渲染进程
从主进程发送消息到渲染进程时,您需要指定哪个渲染进程接收该消息。消息需要通过其 WebContents
实例发送到渲染进程。此 WebContents 实例包含一个 send
方法,该方法可以像 ipcRenderer.send
一样使用。
为了演示此模式,我们将构建一个由本地操作系统菜单控制的数字计数器。
对于此演示,您需要向主进程、渲染进程和预加载脚本添加代码。完整的代码如下所示,但我们将在以下部分中分别解释每个文件。
- 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://mdn.org.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>
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
模块发送消息
对于此演示,我们首先需要使用 Electron 的 Menu
模块在主进程中构建自定义菜单,该菜单使用 webContents.send
API 从主进程向目标渲染进程发送 IPC 消息。
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
)。
click: () => mainWindow.webContents.send('update-counter', -1)
确保您正在加载 index.html
和 preload.js
入口点以进行以下步骤!
2. 通过预加载暴露 ipcRenderer.on
与前面的渲染进程到主进程示例一样,我们在预加载脚本中使用 contextBridge
和 ipcRenderer
模块将 IPC 功能暴露给渲染进程
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
加载预加载脚本后,您的渲染进程应该可以访问 window.electronAPI.onUpdateCounter()
监听器函数。
出于安全原因,我们不会直接公开整个 ipcRenderer.on
API。请确保尽可能限制渲染进程对 Electron API 的访问。此外,不要将回调直接传递给 ipcRenderer.on
,因为这会通过 event.sender
泄漏 ipcRenderer
。使用自定义处理程序,仅使用所需的参数调用 callback
。
在此最小示例中,您可以直接在预加载脚本中调用 ipcRenderer.on
,而不是通过上下文桥将其公开。
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 相比,这种方法的灵活性有限,因为您的监听器无法直接与您的渲染器代码交互。
3. 构建渲染进程 UI
为了将所有内容整合在一起,我们将在加载的 HTML 文件中创建一个界面,其中包含一个 #counter
元素,我们将使用它来显示值
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://mdn.org.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
元素的值。
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
。
可选:返回回复
对于主进程到渲染进程的 IPC,没有等效于 ipcRenderer.invoke
的方法。相反,您可以在 ipcRenderer.on
回调中将回复发送回主进程。
我们可以通过对上一个示例中的代码进行一些修改来演示这一点。在渲染进程中,公开另一个 API 以通过 counter-value
通道将回复发送回主进程。
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
事件并适当地处理它们。
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...
模式 4:渲染进程到渲染进程
Electron 没有使用 ipcMain
和 ipcRenderer
模块在渲染进程之间直接发送消息的方法。要实现此目的,您有两个选择
- 使用主进程作为渲染进程之间的消息代理。这将涉及将消息从一个渲染进程发送到主进程,主进程将转发该消息到另一个渲染进程。
- 将 MessagePort 从主进程传递到两个渲染进程。这将允许在初始设置后渲染进程之间进行直接通信。
对象序列化
Electron 的 IPC 实现使用 HTML 标准 结构化克隆算法 来序列化在进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。
特别是,DOM 对象(例如 Element
、Location
和 DOMMatrix
)、由 C++ 类支持的 Node.js 对象(例如 process.env
、Stream
的某些成员)以及由 C++ 类支持的 Electron 对象(例如 WebContents
、BrowserWindow
和 WebFrame
)无法使用结构化克隆进行序列化。