主进程和渲染进程之间如何通信
在将主进程和渲染进程之间通信之前,大家可以先简单的理解一下什么是主进程,什么是渲染进程,他们之间的关系是什么?
主进程 & 渲染进程
主进程和渲染进程是Electron应用程序中的两个主要进程。
主进程:
- 也称为浏览器进程或主进程。
- 它控制着应用程序的生命周期,显示原生的GUI界面。
- 它管理所有web页面和其他渲染进程。
- 可以与操作系统进行交互,创建菜单,发送通知等。
- 只有一个主进程。
渲染进程:
- 也称为renderer进程或渲染进程。
- 每个web页面运行在它自己的渲染进程里。
- 负责运行和渲染HTML,CSS,JavaScript。
- 无法直接访问原生GUI或操作系统。
- 应用程序可以有多个渲染进程。
主进程和渲染进程之间的关系:
- 主进程会创建一个或多个渲染进程。
- 主进程与渲染进程之间使用IPC(跨进程通信)进行通信。
- 主进程管理所有的页面和渲染进程。
- 主进程可访问原生GUI,而渲染进程不可以。
- 渲染进程负责页面的渲染,主进程负责应用程序的整体控制。
总结: 主进程控制整个应用程序,管理所有页面和渲染进程。渲染进程负责页面的渲染和运行JavaScript。主进程和渲染进程通过IPC通信。这种多进程架构提高了程序的性能、稳定性和安全性。
它们之间如何进行通信
主进程和渲染进程通过IPC通信, 在官方文档中的API里我们可以查阅到,主进程有个ipcMain
, 渲染进程有个ipcRenderer
, 我们可以使用它们来实现主进程和渲染进程之间的通信。
ipcMain
从主进程向渲染进程发送消息,查阅webContents.send获取更多信息。
- 发送消息时,事件名称为
channel
。 - 回复同步信息时,需要设置
event.returnValue
。 - 可以使用
event.reply(...)
将异步消息发送回发送者。 此方法将自动处理从非主 frame 发送的消息(比如: iframes)。相应的发送方法是:event.sender.send(...)
它将总是把消息发送到主 frame
IpcMain
模块有以下方法来侦听事件:
ipcMain.on(channel, listener)
channel
stringlistener
Functionevent
IpcMainEvent...args
any[]
监听 channel, 当新消息到达,将通过 listener(event, args...) 调用 listener。
ipcMain.once(channel, listener)
channel
stringlistener
Functionevent
IpcMainEvent...args
any[]
添加一次性 listener
函数。 这个 listener
只会在 channel
下一次收到消息的时候被调用,之后这个监听器会被移除。
ipcMain.removeListener(channel, listener)
channel
stringlistener
Function...args
any[]
为特定的 channel 从监听队列中删除特定的 listener 监听者.
ipcMain.removeAllListeners([channel])
channel
string (optional)
移除所有指定 channel 的监听器; 若未指定 channel,则移除所有监听器。
ipcMain.handle(channel, listener)
channel
stringlistener
Function<Promise | any>event
IpcMainInvokeEvent...args
any[]
为一个 invokeable
的IPC 添加一个handler。 每当一个渲染进程调用 ipcRenderer.invoke(channel, ...args)
时这个处理器就会被调用。如果 listener
返回一个 Promise,那么 Promise 的最终结果就是远程调用的返回值。 否则, 监听器的返回值将被用来作为应答值。
ipcMain.handleOnce(channel, listener)
channel
stringlistener
Function<Promise | any>event
IpcMainInvokeEvent...args
any[]
处理单个 invoke
able 可触发的 IPC 消息,然后移除侦听器
ipcMain.removeHandler(channel)
channel
string
移除 channel
的所有处理程序,若存在
我们可以看到,关于ipcMain
的监听函数分为on
和handle
, 那么它们的区别是什么呢, on
是处理同步的,handle
处理的是异步
ipcRenderer
介绍完主进程我们介绍一下渲染进程, ipcRenderer
模块使用以下方法来监听事件和发送消息。
ipcRenderer.on(channel, listener)
channel
stringlistener
Functionevent
IpcRendererEvent...args
any[]
监听 channel, 当新消息到达,将通过 listener(event, args...) 调用 listener。
ipcRenderer.once(channel, listener)
channel
stringlistener
Functionevent
IpcRendererEvent...args
any[]
添加一次性 listener
函数。 这个 listener
只会在 channel
下一次收到消息的时候被调用,之后这个监听器会被移除。
ipcRenderer.removeListener(channel, listener)
channel
stringlistener
Function...args
any[]
为特定的 channel 从监听队列中删除特定的 listener 监听者.
ipcRenderer.removeAllListeners(channel)
channel
string
移除所有的监听器,当指定 channel
时只移除与其相关的所有监听器。
ipcRenderer.send(channel, ...args)
channel
string...args
any[]
通过channel
向主进程发送异步消息,可以发送任意参数。 参数将使用 Structured Clone Algorithm进行序列化,就像 window.postMessage
,因此原型链将不会包含在内。 发送 Functions, Promises, Symbols, WeakMaps, 或 WeakSets 将抛出异常。注意: 发送非标准的 JavaScript 类型,如DOM 对象或特殊Electron 对象将会抛出异常。主线程不支持 DOM 对象比如 ImageBitmap
, File
, DOMMatrix
等对象, 它们无法通过Electron的IPC发送到主线程,主要原因是主线程无法对他们进行解码 若尝试通过IPC发送这种对象数据将返回异常
ipcRenderer.invoke(channel, ...args)
channel
string...args
any[]
Returns Promise<any>
- Resolves 主进程返回值通过 channel
向主过程发送消息,并异步等待结果。 参数将使用 Structured Clone Algorithm进行序列化,就像 window.postMessage
,因此原型链将不会包含在内。 发送 Functions, Promises, Symbols, WeakMaps, 或 WeakSets 将抛出异常
ipcRenderer.sendSync(channel, ...args)
channel
string...args
any[]
返回 any
- 由 ipcMain
处理程序发送过来的值。
ipcRenderer.postMessage(channel, message, [transfer])
channel
stringmessage
anytransfer
MessagePort[] (optional)
发送消息到主进程,同时可以选择性发送零到多个 MessagePort
对象从渲染进程发送到主进程的MessagePort
对象可作为MessagePortMain
对象访问触发事件的ports
端口属性
ipcRenderer.sendToHost(channel, ...args)
channel
string...args
any[]
就像 ipcRenderer.send
,不同的是消息会被发送到 host 页面上的 <webview>
元素,而不是主进程。
在渲染进程中,我们的方法发送方法很多,这里我推荐使用invoke
做异步通信,因为同步会造成进程阻塞。
试验
我们现在尝试一下渲染进程给主进程通信, 首先我们要在index.html
中创建一个id为msgBtn
的button
按钮, 在 /renderer/app.js
中我们去拿到这个按钮, 同时绑定一个点击事件, 这里需要注意,由于在渲染进程中我们获取不到 electron
这个对象, 所以我们需要在 preLoad.js
中定义invoke
挂载到window
去使用,此时preLoad.js
代码如下:
arduino
const { contextBridge, ipcRenderer } = require('electron')
const fs = require('path')
console.log(process.platform)
console.log(fs)
console.log(ipcRenderer)
function invoke(channel, someArgument) {
return ipcRenderer.invoke(channel, someArgument)
}
contextBridge.exposeInMainWorld('callNode', {
platform: process.platform,
invoke,
})
暴露了一个invoke
对象,是一个函数,用法与ipcRenderer.invoke
一致,那么此时我们就可以在app.js
中去调用这个它了, 通过channel
向主进程发送异步消息,可以发送任意参数,我们就发送一个'hello, main'
,由于我们的 invoke
是异步的,它是一个promise
我们可以通过.then
的方式拿到主进程返回给我们的消息
javascript
console.log(window.callNode)
const msgBtn = document.querySelector('#msgBtn')
msgBtn.addEventListener('click', () => {
window.callNode.invoke('send-event', 'hello main').then((res) => {
console.log(res)
})
})
目前渲染进程已经把消息发送出去了,我们该怎么接收呢,在主进程main.js
中,我们通过 ipcMain
去定义一个监听事件handle
, 此时我们的main.js
如下:
javascript
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
const createWindows = () => {
const win = new BrowserWindow({
width: 1280,
height: 800,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preLoad.js'),
},
})
win.loadFile('index.html')
win.webContents.openDevTools()
}
app.whenReady().then(() => {
createWindows()
createWindows()
// 仅 macOS 支持
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindows()
}
})
})
app.on('window-all-closed', () => {
// 对于 Mac 系统, 关闭窗口时并不会直接退出应用, 此时需要我们来手动处理
if (process.platform === 'darwin') {
console.log('close')
app.quit()
}
})
ipcMain.handle('send-event', (event, data) => {
console.log(data)
return 'hello render'
})
我们通过handle
回调拿到了渲染进程发送过来的消息数据, 同时通过 return
给渲染进程发送了一条消息, 点击按钮,此时就可以看到在bash
中打印了'hello main'
, 同时窗口控制台也打印了'hello render'
, 那么到底, 我们的通信已经成功了