✧ 渲染进程向主进程的单向通信
在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() :我们所使用的单向通信也可以用来执行双向通信。这是在Electron 7之前通过IPC进行异步双向通信的推荐方式。
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模块发送消息,而且这种通信方式其实也非常少用。要做到这一点,你可以使用主进程作为渲染程序之间的消息代理。这将涉及到从一个渲染器向主进程发送消息,主进程将把消息转发给另一个渲染器,这里就不做演示了。