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 win
app.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 work
console.log('render2main', a, b)
setTimeout(() => {
// main to render
e.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 main
ipcRenderer.send('render2main', 1, 2)
ipcRenderer.on('main2render', (e, d) => {
resultVal.innerHTML = d
console.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.invoke
ipcMain.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 win
app.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 win
app.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, win2
app.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').win2Id
console.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,最后才能发起通信请求;