0042. 通过主进程转发消息的方式实现两个渲染进程之间互相通信
- 本节一共有 2 个 demo
- demos.1 的写法是存在一些问题的(需要理解 demos.1 的写法存在的问题)
- demos.2 对 demos.1 的问题进行了修复(需要理解 demos.2 是如何解决 demos.1 遇到的问题的)
- 类似于 demos.2 的效果,在工作中实践过,使用起来感觉没啥问题,还 OK。需要注意的是 channel 的语义化,如果 channel 的数量比较多,那么可以尝试通过一些特殊的自定义命名规则来区分哪些 channel 是用于在两个渲染进程之间互相通信的。当然,除了通过自定义命名规范来区分,还可以单独写一个全局对象来存储 channel,比如
CHANNEL_LIST.r2r.xxx
、CHANNEL_LIST.r2m.xxx
、CHANNEL_LIST.m2r.xxx
。
1. 📺 视频 - demos.1
2. 💻 demos.1
js
const { app, BrowserWindow, ipcMain } = require('electron')
let win1, win2
function createWin() {
win1 = new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false },
})
win2 = new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false },
})
win1.webContents.openDevTools()
win2.webContents.openDevTools()
win1.loadFile('./index1.html')
win2.loadFile('./index2.html')
}
function handleIPC() {
ipcMain.handle('message-from-renderer1', async (_, ...args) => {
console.log('main process received message from renderer1 with args:', args)
win2.webContents.send('message-to-renderer2', ...args)
return new Promise((resolve) => ipcMain.on('message-from-renderer2', (_, result) => resolve(result)))
})
}
app.whenReady().then(() => {
createWin()
handleIPC()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>renderer2</title>
</head>
<body>
<h1>renderer2</h1>
<script src="renderer2.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
js
const { ipcRenderer } = require('electron')
ipcRenderer.on('message-to-renderer2', (event, ...args) => {
console.log('message-to-renderer2', ...args)
ipcRenderer.send('message-from-renderer2', args.reduce((a, b) => a + b, 0))
})
1
2
3
4
5
6
2
3
4
5
6
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>renderer1</title>
</head>
<body>
<h1>renderer1</h1>
<!-- <script src="renderer1.1.js"></script> -->
<!-- 错误测试用例示范 -->
<script src="renderer1.2.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js
const { ipcRenderer } = require('electron')
async function test() {
console.log('1 + 2 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2))
}
test() // => 1 + 2 = 3
1
2
3
4
5
6
7
2
3
4
5
6
7
- 通信原理图:
- 缺陷分析:
- 修改渲染进程
index1.html
的脚本renderer1.2.js
,试着同时多发几个任务,修改后的内容如下。
- 修改渲染进程
js
// renderer1.2.js
const { ipcRenderer } = require('electron')
async function test1() {
console.log('1 + 2 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2))
}
async function test2() {
console.log('1 + 2 + 3 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2, 3))
}
test1() // => 1 + 2 = 3
test2() // => 1 + 2 + 3 = 3 ❌
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
3. 📺 视频 - demos.2
4. 💻 demos.2
- 就 demos.1 中的问题,我们针对性地处理一下,解决方案也非常简单,给每一个任务加上 id,区分开不同的任务即可。
- 事先下载一个第三方包 uuid,用于生成任务的唯一 id。
json
{
"name": "renderer2renderer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "electron ."
},
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^29.1.0",
"uuid": "^9.0.1"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js
const { app, BrowserWindow, ipcMain } = require('electron')
const { v4: uuidv4 } = require('uuid')
let win1, win2
function createWin() {
win1 = new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false },
})
win2 = new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false },
})
win1.webContents.openDevTools()
win2.webContents.openDevTools()
win1.loadFile('./index1.html')
win2.loadFile('./index2.html')
}
function handleIPC() {
const promises = new Map()
ipcMain.on('message-from-renderer2', (_, { id, result }) => {
const { resolve } = promises.get(id)
promises.delete(id)
resolve(result)
})
ipcMain.handle('message-from-renderer1', async (_, ...args) => {
console.log('main process received message from renderer1 with args:', args)
return await sendRequestToRenderer2(...args)
})
function sendRequestToRenderer2(...args) {
return new Promise((resolve, reject) => {
const id = uuidv4()
promises.set(id, { resolve, reject })
// 主进程在转发的时候,给每个转发的任务加上 id。
win2.webContents.send('message-to-renderer2', id, ...args)
})
}
}
app.whenReady().then(() => {
createWin()
handleIPC()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
js
const { ipcRenderer } = require('electron')
async function test1() {
console.log('1 + 2 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2))
}
async function test2() {
console.log('1 + 2 + 3 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2, 3))
}
test1() // => 1 + 2 = 3
test2() // => 1 + 2 + 3 = 6
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
js
const { ipcRenderer } = require('electron')
ipcRenderer.on('message-to-renderer2', async (event, id, ...args) => {
console.log('message-to-renderer2', id, ...args)
const result = args.reduce((a, b) => a + b, 0)
// 渲染进程处理任务的时候,会收到主进程传递过来的对应任务 id,任务处理完之后,再将 id 一并携带上,丢给主进程。
ipcRenderer.send('message-from-renderer2', { id, result })
})
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
通信原理图: