IPC 模块通信
- Electron 提供了 IPC 通信模块
- 主进程的 ipcMain
- 渲染进程的 ipcRenderer
- ipcMain、ipcRenderer 都是 EventEmitter 对象
在介绍架构原理时,认识到在 Chromium 中,有 RenderProcessHost 和 RenderProcess 对象来实现 Chromium 的主进程到渲染进程之间的通信。在 Electron 中,我们是使用 ipcMain 和 ipcRenderer 模块来实现主进程和渲染进程之间的通信的。
ipcMain 和 ipcRenderer 它们都是 EventEmitter 对象,所以在写法上,和我们平时写 Node.js 的回调非常类似。
在 Electron 中,ipcMain 和 ipcRenderer 对象都是 EventEmitter 的实例,因此它们都支持事件驱动编程模型。您可以在这些对象上注册事件监听器(使用 .on() 方法),也可以通过调用 .emit() 方法在这些对象上触发事件。这些事件用于在主进程和渲染进程之间进行通信,以便它们可以交换数据和执行操作。在使用 Electron 进行应用程序开发时,经常会使用这些对象来实现进程间通信。
EventEmitter 对象是啥?
参考资料:https://www.runoob.com/nodejs/nodejs-event.html
EventEmitter 是 Node.js 中的一个核心模块,它提供了一种实现观察者模式的机制。在 Node.js 中,许多的对象都是 EventEmitter 的实例,比如 HTTP 服务器对象、客户端对象、文件流对象等等。
EventEmitter 对象可以监听事件,当该事件被触发时,所有已注册的回调函数都会被执行。它主要有三个方法:
on(eventName, listener):绑定事件和事件处理函数。emit(eventName, [arg1], [arg2], [...]):触发事件,并把参数传递给事件处理函数。removeListener(eventName, listener):移除事件处理函数。
开发者可以通过 EventEmitter 对象实现许多复杂的事件处理逻辑,例如自定义事件、事件委托、事件队列等等。
var EventEmitter = require('events').EventEmitter;var event = new EventEmitter();event.on('some_event', function() {console.log('some_event 事件触发');});setTimeout(function() {event.emit('some_event');}, 1000);
渲染进程给主进程发消息
在 Electron 渲染进程中,你可以使用 ipcRenderer 对象给主进程(即主进程中的 ipcMain)发送消息。常见的写法有以下两种:
- callback 写法:
- ipcRenderer.send
- ipcMain.on
- Promise 写法(请求 + 响应):
- ipcRender.invoke
- ipcMain.handle
注意:Promise 写法需要 Electron 的版本 > v7
demo | 回调写法 ipcRender.send、ipcMain.on | 渲染进程给主进程发消息
const {app,BrowserWindow,ipcMain} = require('electron')let winapp.on('ready', () => {win = new BrowserWindow({width: 300,height: 300,webPreferences: {nodeIntegration: true,contextIsolation: false,// devTools: false // 默认值是 true,表示是否允许打开开发者调试工具},})// 打开开发者工具,如果 devTools 是 false 的话,那么将无法打开调试工具win.webContents.openDevTools({mode: "detach"})win.loadFile('./index.html')handleIPC()})function handleIPC() {ipcMain.on('render2main', function (e, a, b) {// do some workconsole.log('render2main', a, b)setTimeout(() => {// main to rendere.reply('main2render', a + b)}, 2000);})}
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>23.04.22</title><style>h1 {text-align: center;padding: 2rem;}</style></head><body><h1>Hello World!</h1><div>result:</div><div id="resultVal"></div><script>const {ipcRenderer} = require('electron')// render to mainipcRenderer.send('render2main', 1, 2)ipcRenderer.on('main2render', (e, d) => {resultVal.innerHTML = dconsole.log('received data => ', d)})</script></body></html>
渲染进程:
- 在渲染进程的 HTML 文件中,通过
require('electron')引入了 Electron 中的 ipcRenderer 对象,然后在发送到主进程的消息中调用了ipcRenderer.send方法,将两个数字作为参数发送给主进程。 - 接着,通过
ipcRenderer.on方法监听主进程发送的回复消息,当收到主进程的回复时,将回复内容打印在控制台并更新 HTML 页面中 id 为resultVal的元素中的内容。
主进程:
- 在主进程中,程序通过
require('electron')引入了 Electron 库,然后在应用程序准备好的事件处理程序中创建一个窗口。然后通过win.loadFile方法将渲染进程的 HTML 文件加载到窗口中。 - 最后,定义了一个名为
handleIPC的函数来处理渲染进程和主进程之间的通信。该函数通过ipcMain.on方法监听来自渲染进程的render2main事件,并在接收到事件时打印两个数字,然后通过e.reply方法发送main2render事件和这两个数字的和给渲染进程。为了模拟一些耗时的操作,主进程在回复消息之前使用setTimeout延迟了 2 秒钟。
demo | Promise 写法 ipcRenderer.invoke、ipcMain.handle | 渲染进程给主进程发消息 ☆☆☆
核心 API
ipcRenderer.invokeipcMain.handle
这种请求 + 响应的通通信方式,在是 v7 之后才出现的,在番茄钟 demo 中使用的就是这种通信方式。
.├── index.html├── index.js├── package-lock.json└── package.json
{"main": "index.js","dependencies": {"electron": "^21.2.0"},"scripts": {"dev": "electron ."}}
const {app,BrowserWindow,ipcMain} = require('electron')let winapp.on('ready', () => {win = new BrowserWindow({width: 300,height: 300,webPreferences: {nodeIntegration: true,contextIsolation: false}})win.loadFile('./index.html')win.webContents.openDevTools()handleIPC()})function handleIPC() {ipcMain.handle('do-some-work', async (e, a, b) => {console.log('main process received data: ', a, b)return 'hahaha'})}
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>22-10-31</title><style>h1 {text-align: center;padding: 2rem;}</style></head><body><h1>Hello World!</h1><script>const {ipcRenderer} = require('electron');(async() => {const res = await ipcRenderer.invoke('do-some-work', 1, 2)console.log('render process received data: ', res)})()</script></body></html>
主进程在终端打印的内容:main process received data: 1 2
渲染进程在浏览器调试工具中打印的内容:render process received data: hahaha
主进程给渲染进程发消息
- 渲染进程:ipcRenderer.on
- 主进程:webContents.send
前面介绍的是渲染进程如何向主进程发消息,下面介绍主进程如何向渲染进程发消息。
这里有一点需要时刻注意:主进程只有一个,渲染进程可以有多个。所以不难得出以下结论:
- 从渲染进程向主进程发消息时,直接发消息,主进程就可以收到消息,因为主进程就一个;
- 从主进程向渲染进程发消息时,需要标识出向哪个渲染进程发消息,因为渲染进程有多个;
🤔 如果主进程也是直接发消息的话,这个消息发给哪个渲染进程呢?
Electron 中,我们从主进程向渲染进程发消息的写法其实也很简单,就是通过渲染进程窗口实例身上的 webContents.send 来发送请求。窗口实例其实就是在标识出向哪个渲染进程发消息。
.├── index.html├── index.js├── package-lock.json└── package.json
{"main": "index.js","dependencies": {"electron": "^21.2.0"},"scripts": {"dev": "electron ."}}
const {app,BrowserWindow,ipcMain} = require('electron')let winapp.on('ready', () => {win = new BrowserWindow({width: 300,height: 300,webPreferences: {nodeIntegration: true,contextIsolation: false}})win.loadFile('./index.html')win.webContents.openDevTools()handleIPC()})function handleIPC() {win.webContents.send('do-some-work', 1, 2)}
win 就是渲染进程的窗口实例,我们通过 win.webContents.send('do-some-render-work') 向指定的渲染进程发消息。这个渲染进程对应的页面,其实可以通过 win.loadFile('./index.html') 得知是 index.html 页面,向该渲染进程发消息,其实可以理解为向 index.html 页面发消息。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>22-10-31</title><style>h1 {text-align: center;padding: 2rem;}</style></head><body><h1>Hello World!</h1><script>const {ipcRenderer} = require('electron')ipcRenderer.on('do-some-work', (e, a, b) => {alert('do-some-work')console.log(e, a, b)})</script></body></html>
启动应用:npm run dev
结果:

渲染进程和渲染进程之间发消息(页面间通信)
- 通知事件
- 通过主进程转发(Electron 5之前)
- ipcRenderer.sendTo(Electron 5之后)
- 数据共享
- Web 技术(LocalStorage、sessionStorage、indexedDB)
- 使用 remote(不推荐)
对于数据共享,有两种方式可以实现,第一种就是我们熟知的 Web 技术,比如 localStorage、sessionStorage、indexedDB 等等。还有一种方式是使用 remote 模块,使用它可以将我们需要共享的变量丢到全局,这样就可以在所有模块中访问了。
注意:不推荐使用 remote 这种方式,因为如果 remote 使用不当,很可能会导致程序卡顿,影响性能。
以下是 demo 源码部分:
.├── index1.html├── index2.html├── main.js├── package-lock.json├── package.json├── readme.md├── renderer1.js├── renderer2.js└── yarn.lock
{"name": "22-07-30-3","version": "1.0.0","main": "main.js","license": "MIT","dependencies": {"@electron/remote": "^2.0.8","electron": "^19.0.10"},"scripts": {"dev": "electron ."}}
const {app,BrowserWindow} = require("electron")require('@electron/remote/main').initialize()let win1, win2app.whenReady().then(() => {const opt = {webPreferences: {nodeIntegration: true,contextIsolation: false,// enableRemoteModule: true// https://www.electronjs.org/docs/latest/breaking-changes#default-changed-enableremotemodule-defaults-to-false}}win1 = new BrowserWindow(opt)win1.loadFile('./index1.html')win1.webContents.openDevTools()require("@electron/remote/main").enable(win1.webContents)win2 = new BrowserWindow(opt)win2.loadFile('./index2.html')win2.webContents.openDevTools()global.shareObj = {win2Id: win2.webContents.id}})
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>renderer1</title></head><body><script>const {ipcRenderer} = require('electron')const {getGlobal} = require('@electron/remote')const win2Id = getGlobal('shareObj').win2Idconsole.log('renderer1', win2Id)ipcRenderer.sendTo(win2Id, 'do-some-work', 1, 2)</script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>renderer2</title></head><body><script>const {ipcRenderer} = require('electron')ipcRenderer.on('do-some-work', (e, a, b) => {console.log('trigger do-some-work', e, a, b)})</script></body></html>
启动工程:npm run dev
结果:
⚠️ ⚠️ ⚠️ 这一个 demo,坑很多,主要是官方想方设法让我们不好直接使用 remote 模块而设置的层层阻碍。因为 remote 模块如果使用不当的话,应用的坑可能会更多。
踩过的坑,都记录在 readme.md 里边了 👉 链接
小结
IPC(Inter-Process Communication,进程间通信)

需要掌握下面几点:
- 主进程向渲染进程发消息
- 主进程向渲染进程1发请求
win1.webContents.send(channel, ...args)通过 win1 找到渲染进程1 - 主进程向渲染进程2发请求
win2.webContents.send(channel, ...args)通过 win2 找到渲染进程2 - 某个渲染进程监听请求
ipcRenderer.on(channel, (...args) => {})
- 主进程向渲染进程1发请求
- 渲染进程向主进程发消息
- 主进程监听请求
ipcMain.on(channel, (...args) => {}) - 某个渲染向主进程发请求
ipcRenderer.send(channel, ...args)
- 主进程监听请求
- 渲染进程和渲染进程之间互发消息
- 某个渲染进程监听 channel 请求
ipcRenderer.on(channel, (...args) => {}) - 某个渲染进程向其它渲染进程发 channel 请求
ipcRenderer.sendTo(webContentsId, channel, ...args)
- 某个渲染进程监听 channel 请求
始终记住一点:主进程只有一个,渲染进程可以有多个。
渲染进程之间相互通信,要借助主进程来获取到对应渲染进程的 webContentsId 才行,这样才能知道通信的对象是谁。而渲染进程和主进程之间通信,直接 send 就好了,因为主进程就那么一个。
知道它们之间的数量关系后,理解起来就非常简单了。
- 主进程如果想要和渲染进程通信,因为渲染进程有多个,我们就得告诉它,具体和哪个渲染进程通信;
- 渲染进程和主进程通信,因为主进程只有一个,渲染进程直接发起请求,自然就可以定位到主进程,压根就不用找;
- 渲染进程和渲染进程通信,同样的道理,因为渲染进程有多个,所以必须要指定通信的对象是哪个渲染进程;
渲染进程之间的通信,需要主进程间接协助: 仅仅通过渲染进程自身是没法得知其它渲染进程的 id 的,但是主进程可以获取到所有渲染进程的 id,所以需要先在主进程中,将 id 给保存到全局 global 中,然后在渲染进程中通过 remote 远程模块获取目标渲染进程的 id,最后才能发起通信请求;
