0034. 仿观察者模式实现两个渲染进程之间的互相通信
1. 🔍 查看官方文档对 BrowserWindow.fromId(id)
此 API 的描述
2. 📒 原理简述
- 主进程维护一个事件登记表
messageChannelRecord
,需要监听action
事件的渲染进程在页面加载完毕后立刻通知主进程,主进程记录action
事件和对应渲染进程的 IDe.sender.id
。当某个渲染触发action
事件的时候,主进程根据记录的 ID 逐个去通知注册了该事件的渲染进程。 - 其中 messageChannelRecord 的数据结构如下:
js
const messageChannelRecord:Record<string, Electron.BrowserWindow.id[]> = {}
messageChannelRecord['action'] = [ e.sender.id ]
// Electron.BrowserWindow.id 是 number 类型
1
2
3
2
3
- 注册环节基本流程:
- 触发环节基本流程:
3. 💻 demos.1 - 仿观察者模式实现两个渲染进程之间的互相通信
- 开始是想要直接在主进程中使用 nodejs 的 EventEmitter 模块来实现一个事件总线的效果,但测试时才意识到函数没法直接作为 IPC 的参数来传递,渲染进程的 func 还得放在渲染进程。于是想到通过让主进程来维护一张“事件 <-> 渲染进程 ID”的表,来模拟观察者模式实现通信。
- 这个 demo 并不完善,并没有加上移除事件的方法,仅仅是加了注册事件和触发事件的逻辑。
js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const messageChannelRecord = {}
function createWindow(filePath) {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
})
win.loadFile(filePath)
win.webContents.openDevTools()
}
function handleIPC () {
ipcMain.handle('registerChannelEvent', (e, channel) => {
// 【注意】区分 win.webContents.id 和 win.id
// e.senderer.id 是 win.webContents.id
// BrowserWindow.fromWebContents(e.sender).id 是 win.id
// 通过 BrowserWindow.fromId(ID) 来查询 BrowserWindow 实例,所需的 ID 是 win.id
// 记录注册了 channel 事件的渲染进程的 win.id
if (messageChannelRecord[channel]) {
messageChannelRecord[channel].push(BrowserWindow.fromWebContents(e.sender).id)
} else {
messageChannelRecord[channel] = [BrowserWindow.fromWebContents(e.sender).id]
}
console.log('messageChannelRecord:', messageChannelRecord)
})
ipcMain.handle('emitterChannelEvent', (e, channel, data) => {
// console.log(BrowserWindow.getAllWindows().map(win => ({ winId: win.id, webContentsId: win.webContents.id })))
// 检查记录表 messageChannelRecord 中是否存在 channel 事件
if (messageChannelRecord[channel]) {
// 逐个通知注册了 channel 事件的渲染进程
messageChannelRecord[channel].forEach(id => {
// 前面记录的 win.id 的作用主要是在这一步用于查询 BrowserWindow 实例(🤔 貌似也可以直接在 messageChannelRecord 中存储 BrowserWindow 实例,这样好像还能省略掉查询的开销,不过会导致存储开销增大。)
let win = BrowserWindow.fromId(id)
if (win) {
// 通知注册了 channel 事件的渲染进程
win.webContents.send(channel, data)
}
})
}
})
}
app.whenReady().then(() => {
;[
path.join(__dirname, './renderer/win1/index.html'),
path.join(__dirname, './renderer/win2/index.html'),
path.join(__dirname, './renderer/win3/index.html'),
].forEach((filePath) => createWindow(filePath));
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
52
53
54
55
56
57
58
59
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
52
53
54
55
56
57
58
59
js
const { ipcRenderer } = require('electron')
ipcRenderer.on('action', (e, data) => {
document.querySelector('h1').innerHTML = data
})
ipcRenderer.invoke('registerChannelEvent', 'action')
document.getElementById('btn').addEventListener('click', () => {
ipcRenderer.invoke('emitterChannelEvent', 'action', 1)
})
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
js
const { ipcRenderer } = require('electron')
ipcRenderer.on('action', (e, data) => {
document.querySelector('h1').innerHTML = data
})
ipcRenderer.invoke('registerChannelEvent', 'action')
document.getElementById('btn').addEventListener('click', () => {
ipcRenderer.invoke('emitterChannelEvent', 'action', 2)
})
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
js
const { ipcRenderer } = require('electron')
ipcRenderer.on('action', (e, data) => {
document.querySelector('h1').innerHTML = data
})
ipcRenderer.invoke('registerChannelEvent', 'action')
document.getElementById('btn').addEventListener('click', () => {
ipcRenderer.invoke('emitterChannelEvent', 'action', 3)
})
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
最终效果
- 点击【窗口 1】的按钮,所有窗口的
<h1>
的内容都变为 1。 - 点击【窗口 2】的按钮,所有窗口的
<h1>
的内容都变为 2。 - 点击【窗口 3】的按钮,所有窗口的
<h1>
的内容都变为 3。