进程间通信
进程间通信 (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://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. Listen for events with ipcMain.on
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
回调函数有两个参数:一个 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://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>
为了使这些元素具有交互性,我们将在导入的 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://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
监听事件
在主进程中,我们将创建一个 handleFileOpen()
函数,它调用 dialog.showOpenDialog
并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile
通道发送 ipcRender.invoke
消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke
调用。
在主进程中通过 handle
引发的错误是不透明的,因为它们被序列化了,并且只有原始错误的 message
属性会提供给渲染器进程。 详情请参阅
[#24427](https://github.com/electron/electron/issues/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://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>
用户界面包含一个 #btn
按钮元素,将用于触发我们的预加载 API,以及一个 #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 的一种开发人员友好的方式。 However, a couple of alternative approaches to this IPC pattern exist.
我们建议尽可能 使用 ipcRenderer.invoke
。 出于保留历史的目地,记录了下面双向地渲染器到主进程模式。
对于以下示例,我们将直接从预加载脚本调用 ipcRenderer
,以保持代码示例短小。
使用 ipcRenderer.send
我们用于单向通信的 ipcRenderer.send
API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // 在 DevTools 控制台中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // 在 Node 控制台中打印“ping”
// 作用如同 `send`,但返回一个消息
// 到发送原始消息的渲染器
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) // 在 Node 控制台中打印“ping”
event.returnValue = 'pong'
})
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制台中打印“pong”
这份代码的结构与 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://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)
})