electron + react 开发实践:electron 进程通信

个人的electron + react 开发实践记录
项目使用 electron-react-boilerplate 模板

Electron

官网说法:Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

我的理解Electron其实就是一个浏览器(完全不用考虑兼容性),提供了一部分native api以及集成了nodejs环境,有前端开发经验的人,都可以使用Electron来开发桌面端的应用。

Electron 继承了 Chromium 的多进程架构,分为 主进程渲染进程

主进程

每个 Electron 应用都只有一个主进程,在主进程里,可以直接使用 Node.js API 的功能。

  • 主进程管理窗口
  • 管理应用的生命周期
  • 提供原生API接口

渲染进程

与之对应的,一个应用可以有多个渲染进程,每打开一个 BrowserWindow 都有一个渲染进程运行。
渲染器进程默认是无法访问 Node.js API 的,但是有2个方式可以开启访问权限

  • 设置 nodeIntegration 为 true
  • 使用 preload

Preload脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。为了避免Node.js API泄漏给网页,推荐使用 contextBridge 来实现交互:

main.ts
1
2
3
4
5
6
7
8
import { BrowserWindow } from 'electron'
//...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
//...
preload.ts
1
2
3
4
5
6
7
import { contextBridge } from 'electron'

contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
test: 'test'
}
})
renderer.ts
1
console.log(window.electron.ipcRenderer.test) // test

此功能对两个主要目的來說非常有用:

  • 通过暴露 ipcRenderer 帮手模块于渲染器中,您可以使用 进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 ) 。
  • 如果您正在为远程 URL 上托管的现有 web 应用开发 Electron 封裝,则您可在渲染器的 window 全局变量上添加自定义的属性,好在 web 客户端用上仅适用于桌面应用的设计逻辑 。

进程间通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

主进程与渲染进程通信

ipcMain.handle & ipcRenderer.invoke(🌟墙裂推荐)

刚开始的时候,照着文档的 ipcMain.on 就开始写交互了,期间异常痛苦,各种channel交织在一起,人都写麻了。直到偶然间发现了 ipcMain.handle 这个api,较之 ipcMain.on 优雅了很多,而且也更易用了。
而且如果 listener 返回的是 promise,那么promise最终的结果就是返回值,这个对异步处理非常友好。

main.ts
1
2
3
4
5
6
7
8
9
10
import { ipcMain } from 'electron'

ipcMain.on('handle-message', async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('嘻嘻嘻😁')
}, 1000)
})
})
// ...
preload.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
invoke: async (channel: string, ...args: any[]) => {
const res = await ipcRenderer.invoke(channel, ...args)
return new Promise((resolve, reject) => {
if (res === undefined || res === null) {
reject(res)
} else {
resolve(res)
}
})
},
// ...
}
})
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useEffect, useState } from 'react'

export default function App() {
const [message, setMessage] = useState('')
useEffect(() => {
const getMesasge = async () => {
const msg = await window.electron.ipcRenderer.invoke('handle-message')
setMessage(msg as string)
return msg
}
getMesasge()
}, [])
return <div>{message}</div>
}

展示结果如下

ipcMain.on 与 ipcRenderer.send

这个是旧版本的异步通信方式,单向通信时使用,也可以用于双向通信。但是双向通信时,需要额外的添加一个ipcRenderer的事件监听,很麻烦。

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import { BrowserWindow, ipcMain } from 'electron'

// 单向通信
ipcMain.on('single-message', (_event, message) => {
console.log(message)
})

// 双向通信
ipcMain.on('wuhu-message', (event, arg) => {
event.reply('wuhu-message-reply', 'hhhh')
})

// ...
preload.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
sendSingleMessage: (msg) => {
ipcRenderer.send('single-message', msg)
},
sendWuhuMessage: () => {
ipcRenderer.send('wuhu-message')
}
on: (channel, listener) => {
ipcRenderer.on(channel, (_event, ...args) => listener(...args))
}
}
})
App.tsx
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
import { useState } from 'react'

export default function App () {
const [inputValue, setInputValue] = useState('')
const [msg, setMsg] = useState('')

const sendMsg = () => {
window.electron.ipcRenderer.sendSingleMessage(inputValue)
}

useEffect(() => {
// 双向通信
window.electron.ipcRenderer.on('wuhu-message-reply', (msg) => {
setMsg(msg)
})
setTimeout(() => {
window.electron.ipcRenderer.sendWuhuMessage()
}, 1000)
}, [])
return (
<div>
Title: <input id="title" onChange={e => setInputValue(e.target.value)}/>
<button id="btn" type="button" onClick={sendMsg}>Set</button>
<div>msg: <span id="reply-msg">{msg}</span></div>
</div>
)
}

这里有个坑,虽然是异步的,但是函数内部并不支持 async/await。看下方的代码,最终在 ipcRenderer 中输出的是 undefined。

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { ipcMain } from 'electron'
async function getName () {
return new Promise((resolve) => {
setTimeout(() => {
resolve('2333')
}, 1000)
})
}
ipcMain.on('async-message', async (event) => {
const name = await getName()
event.reply('async-message-reply', name)
})
preload.ts
1
2
3
4
5
6
7
8
import { ipcRenderer } from 'electron'

ipcRenderer.on('async-message-reply', (_event, name) => {
console.log(name) // undefined
})

ipcRenderer.send(`async-message`)

那么想要在 ipcMain 中使用 async/await,就可以用 ipcRenderer.sendSync 来发起同步通信,上边的代码就能正确输出name了。但是使用 ipcRenderer.sendSync 会阻塞进程,还是尽量少用这个api。

所以还是使用多 ipcMain.handle 这个API吧!

小芝士

ipcMain 和 ipcRenderer 都是 EventEmitter 的实例,除了文档里的那些 api, EventEmitter 的其他方法都可以用,比如 emit ,但是 emit 只会触发 ipcMain.on 添加的事件,ipcMain.handle 的不会触发。ipcRenderer.emit 也同样如此

ipcMain.emit / ipcRenderer.emit

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ipcMain } from 'electron'

ipcMain.handle('test-handle', () => {
console.log(1)
})

ipcMain.on('test-on', () => {
console.log(2)
})

ipcMain.emit('test-handle')
ipcMain.emit('test-on') // 观察命令行,会输出 2

// ...