IPC 模块通信

  • Electron 提供了 IPC 通信模块
    • 主进程的 ipcMain
    • 渲染进程的 ipcRenderer
  • ipcMain、ipcRenderer 都是 EventEmitter 对象

在介绍架构原理时,认识到在 Chromium 中,有 RenderProcessHostRenderProcess 对象来实现 Chromium 的主进程到渲染进程之间的通信。在 Electron 中,我们是使用 ipcMainipcRenderer 模块来实现主进程和渲染进程之间的通信的。

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 对象实现许多复杂的事件处理逻辑,例如自定义事件、事件委托、事件队列等等。

  1. var EventEmitter = require('events').EventEmitter;
  2. var event = new EventEmitter();
  3. event.on('some_event', function() {
  4. console.log('some_event 事件触发');
  5. });
  6. setTimeout(function() {
  7. event.emit('some_event');
  8. }, 1000);

渲染进程给主进程发消息

在 Electron 渲染进程中,你可以使用 ipcRenderer 对象给主进程(即主进程中的 ipcMain)发送消息。常见的写法有以下两种:

  • callback 写法:
    • ipcRenderer.send
    • ipcMain.on
  • Promise 写法(请求 + 响应):
    • ipcRender.invoke
    • ipcMain.handle

注意:Promise 写法需要 Electron 的版本 > v7

demo | 回调写法 ipcRender.send、ipcMain.on | 渲染进程给主进程发消息

demo.zip

  1. const {
  2. app,
  3. BrowserWindow,
  4. ipcMain
  5. } = require('electron')
  6. let win
  7. app.on('ready', () => {
  8. win = new BrowserWindow({
  9. width: 300,
  10. height: 300,
  11. webPreferences: {
  12. nodeIntegration: true,
  13. contextIsolation: false,
  14. // devTools: false // 默认值是 true,表示是否允许打开开发者调试工具
  15. },
  16. })
  17. // 打开开发者工具,如果 devTools 是 false 的话,那么将无法打开调试工具
  18. win.webContents.openDevTools({
  19. mode: "detach"
  20. })
  21. win.loadFile('./index.html')
  22. handleIPC()
  23. })
  24. function handleIPC() {
  25. ipcMain.on('render2main', function (e, a, b) {
  26. // do some work
  27. console.log('render2main', a, b)
  28. setTimeout(() => {
  29. // main to render
  30. e.reply('main2render', a + b)
  31. }, 2000);
  32. })
  33. }
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>23.04.22</title>
  6. <style>
  7. h1 {
  8. text-align: center;
  9. padding: 2rem;
  10. }
  11. </style>
  12. </head>
  13. <body>
  14. <h1>Hello World!</h1>
  15. <div>result:</div>
  16. <div id="resultVal"></div>
  17. <script>
  18. const {
  19. ipcRenderer
  20. } = require('electron')
  21. // render to main
  22. ipcRenderer.send('render2main', 1, 2)
  23. ipcRenderer.on('main2render', (e, d) => {
  24. resultVal.innerHTML = d
  25. console.log('received data => ', d)
  26. })
  27. </script>
  28. </body>
  29. </html>

渲染进程:

  1. 在渲染进程的 HTML 文件中,通过 require('electron') 引入了 Electron 中的 ipcRenderer 对象,然后在发送到主进程的消息中调用了 ipcRenderer.send 方法,将两个数字作为参数发送给主进程。
  2. 接着,通过 ipcRenderer.on 方法监听主进程发送的回复消息,当收到主进程的回复时,将回复内容打印在控制台并更新 HTML 页面中 id 为 resultVal 的元素中的内容。

主进程:

  1. 在主进程中,程序通过 require('electron') 引入了 Electron 库,然后在应用程序准备好的事件处理程序中创建一个窗口。然后通过 win.loadFile 方法将渲染进程的 HTML 文件加载到窗口中。
  2. 最后,定义了一个名为 handleIPC 的函数来处理渲染进程和主进程之间的通信。该函数通过 ipcMain.on 方法监听来自渲染进程的 render2main 事件,并在接收到事件时打印两个数字,然后通过 e.reply 方法发送 main2render 事件和这两个数字的和给渲染进程。为了模拟一些耗时的操作,主进程在回复消息之前使用 setTimeout 延迟了 2 秒钟。

demo | Promise 写法 ipcRenderer.invoke、ipcMain.handle | 渲染进程给主进程发消息 ☆☆☆

核心 API

  • ipcRenderer.invoke
  • ipcMain.handle

这种请求 + 响应的通通信方式,在是 v7 之后才出现的,在番茄钟 demo 中使用的就是这种通信方式。

  1. .
  2. ├── index.html
  3. ├── index.js
  4. ├── package-lock.json
  5. └── package.json
  1. {
  2. "main": "index.js",
  3. "dependencies": {
  4. "electron": "^21.2.0"
  5. },
  6. "scripts": {
  7. "dev": "electron ."
  8. }
  9. }
  1. const {
  2. app,
  3. BrowserWindow,
  4. ipcMain
  5. } = require('electron')
  6. let win
  7. app.on('ready', () => {
  8. win = new BrowserWindow({
  9. width: 300,
  10. height: 300,
  11. webPreferences: {
  12. nodeIntegration: true,
  13. contextIsolation: false
  14. }
  15. })
  16. win.loadFile('./index.html')
  17. win.webContents.openDevTools()
  18. handleIPC()
  19. })
  20. function handleIPC() {
  21. ipcMain.handle('do-some-work', async (e, a, b) => {
  22. console.log('main process received data: ', a, b)
  23. return 'hahaha'
  24. })
  25. }
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>22-10-31</title>
  6. <style>
  7. h1 {
  8. text-align: center;
  9. padding: 2rem;
  10. }
  11. </style>
  12. </head>
  13. <body>
  14. <h1>Hello World!</h1>
  15. <script>
  16. const {
  17. ipcRenderer
  18. } = require('electron')
  19. ;(async() => {
  20. const res = await ipcRenderer.invoke('do-some-work', 1, 2)
  21. console.log('render process received data: ', res)
  22. })()
  23. </script>
  24. </body>
  25. </html>

主进程在终端打印的内容:main process received data: 1 2
渲染进程在浏览器调试工具中打印的内容:render process received data: hahaha

主进程给渲染进程发消息

  • 渲染进程:ipcRenderer.on
  • 主进程:webContents.send

前面介绍的是渲染进程如何向主进程发消息,下面介绍主进程如何向渲染进程发消息。

这里有一点需要时刻注意:主进程只有一个,渲染进程可以有多个。所以不难得出以下结论:

  • 从渲染进程向主进程发消息时,直接发消息,主进程就可以收到消息,因为主进程就一个;
  • 从主进程向渲染进程发消息时,需要标识出向哪个渲染进程发消息,因为渲染进程有多个;

🤔 如果主进程也是直接发消息的话,这个消息发给哪个渲染进程呢?
Electron 中,我们从主进程向渲染进程发消息的写法其实也很简单,就是通过渲染进程窗口实例身上的 webContents.send 来发送请求。窗口实例其实就是在标识出向哪个渲染进程发消息。

  1. .
  2. ├── index.html
  3. ├── index.js
  4. ├── package-lock.json
  5. └── package.json
  1. {
  2. "main": "index.js",
  3. "dependencies": {
  4. "electron": "^21.2.0"
  5. },
  6. "scripts": {
  7. "dev": "electron ."
  8. }
  9. }
  1. const {
  2. app,
  3. BrowserWindow,
  4. ipcMain
  5. } = require('electron')
  6. let win
  7. app.on('ready', () => {
  8. win = new BrowserWindow({
  9. width: 300,
  10. height: 300,
  11. webPreferences: {
  12. nodeIntegration: true,
  13. contextIsolation: false
  14. }
  15. })
  16. win.loadFile('./index.html')
  17. win.webContents.openDevTools()
  18. handleIPC()
  19. })
  20. function handleIPC() {
  21. win.webContents.send('do-some-work', 1, 2)
  22. }

win 就是渲染进程的窗口实例,我们通过 win.webContents.send('do-some-render-work') 向指定的渲染进程发消息。这个渲染进程对应的页面,其实可以通过 win.loadFile('./index.html') 得知是 index.html 页面,向该渲染进程发消息,其实可以理解为向 index.html 页面发消息。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>22-10-31</title>
  6. <style>
  7. h1 {
  8. text-align: center;
  9. padding: 2rem;
  10. }
  11. </style>
  12. </head>
  13. <body>
  14. <h1>Hello World!</h1>
  15. <script>
  16. const {
  17. ipcRenderer
  18. } = require('electron')
  19. ipcRenderer.on('do-some-work', (e, a, b) => {
  20. alert('do-some-work')
  21. console.log(e, a, b)
  22. })
  23. </script>
  24. </body>
  25. </html>

启动应用:npm run dev

结果:
image.png

image.png

渲染进程和渲染进程之间发消息(页面间通信)

  • 通知事件
    • 通过主进程转发(Electron 5之前)
    • ipcRenderer.sendTo(Electron 5之后)
  • 数据共享
    • Web 技术(LocalStorage、sessionStorage、indexedDB)
    • 使用 remote(不推荐)

对于数据共享,有两种方式可以实现,第一种就是我们熟知的 Web 技术,比如 localStoragesessionStorageindexedDB 等等。还有一种方式是使用 remote 模块,使用它可以将我们需要共享的变量丢到全局,这样就可以在所有模块中访问了。

注意:不推荐使用 remote 这种方式,因为如果 remote 使用不当,很可能会导致程序卡顿,影响性能。

以下是 demo 源码部分:

  1. .
  2. ├── index1.html
  3. ├── index2.html
  4. ├── main.js
  5. ├── package-lock.json
  6. ├── package.json
  7. ├── readme.md
  8. ├── renderer1.js
  9. ├── renderer2.js
  10. └── yarn.lock
  1. {
  2. "name": "22-07-30-3",
  3. "version": "1.0.0",
  4. "main": "main.js",
  5. "license": "MIT",
  6. "dependencies": {
  7. "@electron/remote": "^2.0.8",
  8. "electron": "^19.0.10"
  9. },
  10. "scripts": {
  11. "dev": "electron ."
  12. }
  13. }
  1. const {
  2. app,
  3. BrowserWindow
  4. } = require("electron")
  5. require('@electron/remote/main').initialize()
  6. let win1, win2
  7. app.whenReady().then(() => {
  8. const opt = {
  9. webPreferences: {
  10. nodeIntegration: true,
  11. contextIsolation: false,
  12. // enableRemoteModule: true
  13. // https://www.electronjs.org/docs/latest/breaking-changes#default-changed-enableremotemodule-defaults-to-false
  14. }
  15. }
  16. win1 = new BrowserWindow(opt)
  17. win1.loadFile('./index1.html')
  18. win1.webContents.openDevTools()
  19. require("@electron/remote/main").enable(win1.webContents)
  20. win2 = new BrowserWindow(opt)
  21. win2.loadFile('./index2.html')
  22. win2.webContents.openDevTools()
  23. global.shareObj = {
  24. win2Id: win2.webContents.id
  25. }
  26. })
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>renderer1</title>
  6. </head>
  7. <body>
  8. <script>
  9. const {
  10. ipcRenderer
  11. } = require('electron')
  12. const {
  13. getGlobal
  14. } = require('@electron/remote')
  15. const win2Id = getGlobal('shareObj').win2Id
  16. console.log('renderer1', win2Id)
  17. ipcRenderer.sendTo(win2Id, 'do-some-work', 1, 2)
  18. </script>
  19. </body>
  20. </html>
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>renderer2</title>
  6. </head>
  7. <body>
  8. <script>
  9. const {
  10. ipcRenderer
  11. } = require('electron')
  12. ipcRenderer.on('do-some-work', (e, a, b) => {
  13. console.log('trigger do-some-work', e, a, b)
  14. })
  15. </script>
  16. </body>
  17. </html>

启动工程:npm run dev
结果:
image.png

⚠️ ⚠️ ⚠️ 这一个 demo,坑很多,主要是官方想方设法让我们不好直接使用 remote 模块而设置的层层阻碍。因为 remote 模块如果使用不当的话,应用的坑可能会更多。

踩过的坑,都记录在 readme.md 里边了 👉 链接

小结

IPC(Inter-Process Communication,进程间通信)

image.png

需要掌握下面几点:

  • 主进程向渲染进程发消息
    • 主进程向渲染进程1发请求 win1.webContents.send(channel, ...args) 通过 win1 找到渲染进程1
    • 主进程向渲染进程2发请求 win2.webContents.send(channel, ...args) 通过 win2 找到渲染进程2
    • 某个渲染进程监听请求 ipcRenderer.on(channel, (...args) => {})
  • 渲染进程向主进程发消息
    • 主进程监听请求 ipcMain.on(channel, (...args) => {})
    • 某个渲染向主进程发请求ipcRenderer.send(channel, ...args)
  • 渲染进程和渲染进程之间互发消息
    • 某个渲染进程监听 channel 请求 ipcRenderer.on(channel, (...args) => {})
    • 某个渲染进程向其它渲染进程发 channel 请求ipcRenderer.sendTo(webContentsId, channel, ...args)

始终记住一点:主进程只有一个,渲染进程可以有多个。

渲染进程之间相互通信,要借助主进程来获取到对应渲染进程的 webContentsId 才行,这样才能知道通信的对象是谁。而渲染进程和主进程之间通信,直接 send 就好了,因为主进程就那么一个。

知道它们之间的数量关系后,理解起来就非常简单了。

  1. 主进程如果想要和渲染进程通信,因为渲染进程有多个,我们就得告诉它,具体和哪个渲染进程通信;
  2. 渲染进程和主进程通信,因为主进程只有一个,渲染进程直接发起请求,自然就可以定位到主进程,压根就不用找;
  3. 渲染进程和渲染进程通信,同样的道理,因为渲染进程有多个,所以必须要指定通信的对象是哪个渲染进程;

渲染进程之间的通信,需要主进程间接协助: 仅仅通过渲染进程自身是没法得知其它渲染进程的 id 的,但是主进程可以获取到所有渲染进程的 id,所以需要先在主进程中,将 id 给保存到全局 global 中,然后在渲染进程中通过 remote 远程模块获取目标渲染进程的 id,最后才能发起通信请求;