欢迎来到Electron入门教程的第三期教程,这一节非常重要!进程间通信(IPC)是在Electron中构建功能丰富的桌面应用程序的关键部分。因为主进程和渲染进程在Electron的进程模型中有不同的职责,IPC是执行许多常见任务的唯一方式,比如从UI调用本地API或从本地菜单触发web内容的更改。下面就来详细介绍3种常见的通信方式。
✧ 渲染进程向主进程的单向通信
在Electron中,进程通过开发人员定义的“通道”与ipcMain
模块和ipcRenderer
模块进行通信。这些通道是任意的(您可以任意命名它们)和双向的(您可以为两个模块使用相同的通道名称)。要从渲染进程向主进程发送单向IPC消息,可以再预渲染脚本preload.js里使用ipcRenderer
发送API发送消息,然后在main.js里用ipcMain.on
接收。你通常使用这个模式从你的web内容中调用一个主进程API。我们将通过创建一个简单的应用程序来演示这种模式,该应用程序可以通过编程方式更改窗口的标题。
下面我们用代码来演示一下这个过程,下面是案例的所有代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>进程通信</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./index.js"></script>
</body>
</html>
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
index.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
});
main.js
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('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()
})
运行效果如下(GIF有点慢,别介意):
下面对代码的一些要点进行讲解:
1.在主进程中监听事件
在主进程中,我们使用ipcMain
在set-title
通道上设置一个IPC监听器,这个set-title
是我们在预渲染脚本preload.js里面定义的接口通道。
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
每当消息通过set-title
通道传入时,此函数将找到附加到消息发送者的BrowserWindow
实例,并使用win.setTitle
设置应用窗口的标题。
2.在预加载脚本里面通过定义接口通道
要向上面创建的侦听器发送消息,您可以使用ipcRenderer
。发送API。默认情况下,渲染器进程没有Node.js或Electron模块访问。作为应用程序开发人员,您需要使用contextBridge
从预加载脚本中选择要公开哪些API。此时,您将能够在呈现过程中使用window.electronAPI.setTitle()
函数。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
✧ 渲染进程与主进程的双向通信
双向IPC的一个常见应用是从渲染进程代码中调用主进程模块并等待结果。这可以通过使用ipcRenderer.invoke
来实现,调用ipcMain.handle
配对。在下面的例子中,我们将从渲染进程中打开一个选择本地文件对话框,并返回所选文件的路径。
下面是案例涉及的所有代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>进程通信</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./index.js'></script>
</body>
</html>
index.js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
main.js
const {app, BrowserWindow, ipcMain,dialog} = require('electron')
const path = require('path')
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (canceled) {
return ""
} else {
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('openFileDialog', handleFileOpen)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI',{
openFile: () => ipcRenderer.invoke('openFileDialog')
})
运行效果演示:
下面对代码的一些要点进行讲解:
1.在主进程定义事件处理函数,并监听ICP接口的调用
在主进程中,我们将创建一个调用dialog
模块的showOpenDialog
方法的函数handleFileOpen()
,用于返回用户选择的文件路径的值。在应用准备好之后,里面调用ipcMain.handle()
来监听渲染进程里的ipcRenderer.invoke('openFileDialog')
里定义的openFileDialog
。当index.js里面调用window.electronAPI.openFile()
时,会触发openFileDialog
,进而被主进程监听处理后,返回结果。
2. 调用通过预加载脚本定义接口
在预加载脚本中,我们公开了一个单行openFile
函数,它调用并返回ipcRederer .invoke('openFileDialog')
。
在index.js代码片段中,我们监听对#btn
按钮的点击,并调用window.electronAPI.openFile()
来激活本地的openFile
对话框。然后在#filePath
元素中显示选定的文件路径。
3. ipcRenderer.invoke的替代ipcRenderer.invoke()
有两种替代方式:
(1)ipcRenderer.send()
preload.js
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg)
// 会打印pong
})
ipcRenderer.send('asynchronous-message', 'ping')
main,js
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg)
//会答应ping
event.reply('asynchronous-reply', 'pong')
})
(1) ipcRenderer.sendSync()
preload.js
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result)
// 会打印pong
main.js
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg)
// 会打印ping
event.returnValue = 'pong'
})
此代码的结构与调用模型非常相似,但出于性能原因,我们建议避免使用此API。它的同步特性意味着它将阻塞呈现程序进程,直到接收到应答。
✧ 主进程向渲染进程的单向通信
当从主进程向渲染进程发送消息时,您需要指定哪个渲染程序正在接收消息。消息需要通过主进程的WebContents
实例发送到渲染进程。这个WebContents
实例包含一个sent
方法,可以像ipcReender .send
那样使用它。为了演示这个通信模式,将构建一个由菜单栏控制的数字计数器。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>进程通信</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src='./index.js'></script>
</body>
</html>
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
index.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
main.js
const {app, BrowserWindow, Menu, ipcMain} = require('electron')
const path = require('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()
})
运行效果演示:
对部分代码讲解:
我们首先需要在主流程中使用Electron的Menu
模块构建一个自定义菜单,从主进程向目标渲染器发送IPC消息。单击处理程序通过计数器通道向呈现程序进程发送消息(1或-1)。
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)
就像前面渲染到主进程的例子一样,我们在预加载脚本preload.js中使用contextBridge
和ipcRederer
模块向渲染进程公开IPC功能:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
✧ 渲染进程之间的通信
在Electron中,没有直接的方法在渲染进程之间使用ipcMain
和ipRenderer
模块发送消息,而且这种通信方式其实也非常少用。要做到这一点,你可以使用主进程作为渲染程序之间的消息代理。这将涉及到从一个渲染器向主进程发送消息,主进程将把消息转发给另一个渲染器,这里就不做演示了。