
个人的electron + react 开发实践记录
项目使用 electron-react-boilerplate 模板
官网说法: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.ts1 2 3 4 5 6 7 8
| import { BrowserWindow } from 'electron'
const win = new BrowserWindow({ webPreferences: { preload: 'path/to/preload.js' } })
|
preload.ts1 2 3 4 5 6 7
| import { contextBridge } from 'electron'
contextBridge.exposeInMainWorld('electron', { ipcRenderer: { test: 'test' } })
|
renderer.ts1
| console.log(window.electron.ipcRenderer.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.ts1 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.ts1 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.tsx1 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.ts1 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.ts1 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.tsx1 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.ts1 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.ts1 2 3 4 5 6 7 8
| import { ipcRenderer } from 'electron'
ipcRenderer.on('async-message-reply', (_event, name) => { console.log(name) })
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.ts1 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')
|