在 electron 中,渲染进程和主进程是两个独立的进程,渲染进程,通常就是我们理解的前端渲染,主进程就是 nodejs 进程。两者之间无法直接互相访问,若需要传输数据,则可以有以下的几种方式。
在 electron 应用中,有很多渲染进程和主进程需要交互的场景,这两者之间该如何通信呢?
1. 渲染进程到主进程的单向通信 #
比如一些窗口事件,最小化、关闭、置顶、切换主题,提交日志等之类的事件。渲染进程告知主进程一件事,而无需关心事件结果的,可以使用 ipcRenderer.send + ipcMain.on。
在渲染进程中,可以使用 ipcRenderer.send 向主进程发送事件。ipcRenderer 也是从 electron 中导出的,因此该方法需要写在 preload.js 中,然后再通过 contextBridge.exposeInMainWorld() 注入到前端页面中:
// preload.js
import { contextBridge, ipcRenderer } from "electron";
const changeTheme = (theme) => {
ipcRenderer.send("changeTheme", theme);
}
contextBridge.exposeInMainWorld("electronAPI", {
changeTheme
});
主进程中使用 ipcMain.on() 接受事件:
// 主进程
import { ipcMain } from "electron";
ipcMain.on("changeTheme", (event, theme) => {
// 切换主题
});
这种从渲染进程进程向主进程发送消息的方式,是非阻塞式的,渲染进程会直接继续执行后续的操作。
2. 渲染进程到主进程的单向通信但有返回值 #
上面的 ipcRenderer.send + ipcMain.on 组合是异步非阻塞式的,渲染进程不关心也拿不到主进程的返回结果。那还有一种方式,是可以拿到返回结果的:ipcRenderer.sendSync。
但这里非常不建议使用这种方式,因为它是阻塞式的,会直接让渲染进程卡住,等待主进程的返回结果
渲染进程:
// preload.js
import { contextBridge, ipcRenderer } from "electron";
const getDataPath = () => {
ipcRenderer.sendSync("getDataPath");
}
contextBridge.exposeInMainWorld("electronAPI", {
getDataPath
});
在主进程中,返回值可以放到 ipcMain.on 的 event.returnValue 中。注意,这是同步进程,回调函数写成 Promise 是没有作用的:
// 主进程
import { ipcMain } from "electron";
ipcMain.on("getDataPath", (event) => {
event.returnValue = global.pathConfig.publicPath;
});
// ❌ Promise 是无效的
ipcMain.on("getDataPath", async (event) => {
// ❌ 无效
const data = await fs.readFile('./xxx.txt')
e.returnValue = data // 无效!同步回调里不能写异步
});
在 html 中再调用 getDataPath() 就可以拿到主进程的返回数据了:
// 前端 index.js
// 同步进程,可以直接拿到返回值
const result = window.electronAPI.getDataPath();
console.log("result", result);
虽然这种方式,可以让渲染进程以同步的方式拿到主进程中返回的数据。但我们极度不推荐这么做。因为这种方式
- 会阻塞渲染进程主线程,大量使用会造成页面卡死、交互卡顿;
- 无法处理异步逻辑,只能同步执行;
- 异常无法优雅捕获,容易直接崩溃渲染进程;
若我们就是需要在渲染进程发起请求,然后拿到主进程的数据,怎么办呢?
3. 渲染进程到主进程的双向异步通信 #
比如读取本地文件、写入文件、获取目录列表、获取本机信息、用户目录、打开文件选择框、保存对话框、网络请求等操作,是需要在渲染进程发起请求,然后在主进程处理并返回结果的,可以使用: ipcRenderer.invoke + ipcMain.handle。
渲染进程中,通过 preload.js 文件向页面中暴露到页面中。ipcRenderer.invoke()永远返回 Promise 对象,必须用 .then() / await 来接收数据:
// preload.js
import { contextBridge, ipcRenderer } from "electron";
// 在渲染进程中向主进程发送异步消息
const ipcInvoke = (channel, ...arg) => {
return ipcRenderer.invoke(channel, ...arg);
};
contextBridge.exposeInMainWorld("electronAPI", {
ipcInvoke
});
html 页面中:
const getOsInfoClick = async () => {
const info = await window.electronAPI.ipcInvoke("getOsInfo");
console.log("info", info);
}
在主进程中,需要使用 ipcMain.handle 监听对应的事件:
// 主进程
import { ipcMain } from "electron";
import os from "node:os";
// 无论这里是直接返回还是以 Promise 返回,在渲染进程都是以 Promise 收到数据
ipcMain.on("getOsInfo", () => {
return {
platform: os.platform(),
arch: os.arch(),
release: os.release(),
hostname: os.hostname(),
userInfo: os.userInfo(),
}
});
在需要渲染进程从主进程获取数据时,推荐使用这种方式。
4. 主进程主动向渲染进程推送数据 #
还有一些场景,是主进程向渲染进程推送数据的,比如定时器、下载进度、多窗口之间通信、系统级原生能力回调(托盘点击、右键菜单等)、全局快捷键、系统剪贴板变更、硬件设备事件、电脑休眠、唤醒、锁屏、解锁、屏幕亮度变化等等。
主进程通过 win.webContents.send('xxx', data) 发送数据,渲染进程用 ipcRenderer.on 接收。若需要把数据通知到多个窗口中,可以先拿到这几个窗口的实例,然后每个实例均调用 win实例.webContents.send()。
4.1 渲染进程中监听事件 #
多个窗口之间是共享 preload.js 的,我们在渲染进程中定义好接收方式,所有窗口里的 html 都可以直接使用:
// preload.js
import { contextBridge, ipcRenderer } from "electron";
// subscribe() 用来订阅主进程发送过来的事件消息,返回值是取消方法
const subscribe = (channel: string, callback: (...args: any[]) => void) => {
const subscription = (_, ...args) => callback(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
};
contextBridge.exposeInMainWorld("electronAPI", {
subscribe
});
在 html 文件中:
const App = () => {
useEffect(() => {
const listener = (data) => {
console.log("主进程推送过来的数据", data);
};
// 监听主进程的事件,并返回取消方法,用来取消监听
const cancel = window.electronAPI.subscribe("main-send-renderer", listener);
return () => {
cancel();
}
}, [])
}
4.2 单个窗口发送数据 #
若应用中只有一个窗口时,或者只向主窗口的页面中发送数据时,可以用主窗口的实例来操作。
// 主进程 main.js
const { BrowserWindow, ipcMain } = require('electron')
const mainWindow = new BrowserWindow()
// 任意时机主动发消息(定时器、托盘、快捷键、其他回调都可以)
setInterval(() => {
mainWindow.webContents.send('main-send-renderer', {
time: new Date().toLocaleString(),
msg: '主进程主动推送'
})
}, 3000)
4.3 向多个窗口发送数据 #
比如有些数据是多个窗口之间共享的,页面显示的状态是要保持一致的,用户在其中一个窗口中操作后,就需要把最新的数据同步到其他窗口中。
// 主进程 main.js
const { BrowserWindow } = require('electron')
function broadcastToAllWindows(channel, data) {
const allWindows = BrowserWindow.getAllWindows()
for (const win of allWindows) {
win.webContents.send(channel, data)
}
}
// 使用
broadcastToAllWindows('global-notice', { text: '全局通知' })
5. 总结 #
今天我们主要了解了在 elecron 中,渲染进程和主进程之间的几种不同的通信方式。