一文带你搞懂Electron如何优雅的进行进程间通讯
作者:若邪
Electron 本身提供的进程间通讯的方法比较偏底层,使用起来有些繁琐、不易维护。Electron 中渲染进程和主进程之间的通讯,其实就像传统 web 开发中网页端与服务器的通讯。那么 Electron 中进程间通讯能不能实现像调用 HTTP 请求一样方便呢,答案是肯定的。
以下是一个 HTTP 请求的简单封装:
import { request } from "@/utils/request"; const { VITE_HOST } = import.meta.env; // #region 操作密码校验接口 export interface ICheckPwdResult { /** * 0:成功,其他:失败 */ code: string; message: string; } export interface ICheckPwdData { /** * 类型:LOCK_SCREEN:锁屏 */ type: string; /** * 密码 */ password: string; } /** * 操作密码校验接口 * /project/2205/interface/api/378926 * @author * * @param {ICheckPwdData} data * @returns */ export function checkPwd(data: ICheckPwdData) { return request<ICheckPwdResult>({ url: `${VITE_HOST}/user/operation/pwd/check`, method: "POST", data, }); } // #endregion
下面是 Electron 中渲染进程向主进程发送请求的封装:
export const setMaximize = () => { return request({ cmd: "setMaximize", }); }; export const setMinimize = () => { return request({ cmd: "setMinimize", }); }; export const closeWindow = () => { return request({ cmd: "closeWindow", }); }; /** * @description 获取 mac 地址 * @returns */ export const getMac = () => { return request<string>({ cmd: "getMac", }); };
下面细说如何封装:
Electron 进程间通讯有四种模式:
- 渲染器进程到主进程(单向)
- 渲染器进程到主进程(双向)
- 主进程到渲染器进程(单向)
- 渲染器进程到渲染器进程
更多细节可以查看 进程间通信 | Electron
单向和双向的区别就是:单向的消息发出去之后是没有消息返回的,或者说拿不到返回的消息;双向就是请求发出后可以拿到响应信息,就跟 HTTP 请求一样。
举个例子,页面向主进程发起请求,希望拿到计算机的 mac 地址,主进程通过调用相应的 nodejs api 拿到 mac 地址返回,页面拿到消息结果,这就是双向的。
我并没有用到原生就支持双向的 2 模式,而是使用都是单向的 1、3 模式,通过封装后实现双向的效果。主要是因为 Electron 并不支持主进程到渲染器进程的双向模式,也就是主进程给页面主动发送消息后是无法拿到响应的。为了保持封装后代码的统一性,都使用了单向的模式进行封装。
结合下图以及实际代码调用,说说是如何基于单向的消息模式实现双向通讯的。
/** * @description 获取 mac 地址 * @returns */ export const getMac = () => { return request<string>({ cmd: "getMac", }); };
const handleGetMac = () => { getMac().then((mac) => { model.mac.value = mac; }); };
页面中通过调用getMac
方法获取计算机 mac 地址。
getMac
方法是一个异步方法,渲染进程在调用这个方法发送消息的时候,会生成一个 uuid,然后将 uuid 、cmd 字段、data 数据(这里没有)放到消息体里再发送给主进程。同时会在回调队列里增加两条回调,一条成功回调,一条失败回调,并且使用 uuid 标识,成功回调就是 then 里面的函数,失败回调就是 catch 里的函数。
主进程接收到消息后,根据cmd
字段做相应的处理,处理完后将结果
和渲染进程发送的消息体里的 uuid
放到响应消息体里,响应消息体里也有 cmd
字段,并且是固定(我使用postMessageCallback
标识),这样渲染进程在接收到消息的时候就知道这是一条之前发送出去的消息的响应。响应消息体里还有一个code
字段标识是成功还是失败。
渲染进程接收到消息后,根据消息体里的 cmd
字段判断消息是响应消息(cmd
为postMessageCallback
)还是普通消息(主进程主动发送的消息)。如果是响应消息,根据 uuid
在回调队列里找到相应的回调,再根据 code
判断是执行成功回调还是失败回调。
以上就是渲染进程到主进程的单向消息模式实现双向通讯的整个通讯过程。把消息的发送、处理、返回响应的对象逆过来就是主进程到渲染进程的双向通讯了。
原理说清楚了,下面就是代码实现了。
渲染进程需要有发送消息、监听消息的功能:
contextBridge.exposeInMainWorld("ipcRenderer", { addEventListener( key: "message", listener: (data: { cmd: string; cbid: string; data: unknown }) => void, ) { return ipcRenderer.on(key, (...args) => { const message = args[1] as { cmd: string; cbid: string; data: unknown }; listener(message); }); }, postMessage(data: { cmd: string; data: unknown; cdid?: string; code?: number; }) { return ipcRenderer.send("message", data); },
这样页面中 window 对象里就有了 ipcRenderer
对象,ipcRenderer
对象提供了 addEventListener
、postMessage
方法,分别用来监听消息和发送消息。
把 addEventListener
、postMessage
再做一下封装,就可以像调用 HTTP 请求一样向主进程发起请求了。
/* eslint-disable no-shadow */ import handle from "./handle"; const callbacks: { [propName: string]: (data: unknown) => void } = {}; const errorCallbacks: { [propName: string]: (data: unknown) => void } = {}; function postMessage( data: { cmd: string; data?: unknown }, cb?: (data: unknown) => void, errorCb?: (data: unknown) => void, ) { if (cb) { const cbid = Date.now().toString(); callbacks[cbid] = cb; window.ipcRenderer?.postMessage({ cmd: data.cmd, data: data.data, cbid: cbid, }); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { window.ipcRenderer?.postMessage({ cmd: data.cmd, data: data.data, }); } } function request<T = unknown>(params: { cmd: string; data?: unknown }) { return new Promise<T>((resolve, reject) => { postMessage( { cmd: params.cmd, data: params.data }, (res) => { resolve(res as T); }, (error) => { reject(error); }, ); }); } function invokeCallback<T = unknown>(cbid: string, res: T) { window.ipcRenderer?.postMessage({ cmd: "postMessageCallback", cbid, data: res, code: 200, }); } function invokeErrorCallback(cbid: string, res: unknown) { window.ipcRenderer?.postMessage({ cmd: "postMessageCallback", cbid, data: res, code: 400, }); } export const addIpcRendererEventListener = () => { window.ipcRenderer?.addEventListener("message", async (message) => { console.log("ipcRenderer get message", message); // 处理主进程主动发的消息 if (message.cmd !== "postMessageCallback") { if (handle[message.cmd]) { try { const res = await handle[message.cmd](message.data); invokeCallback(message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(message.cbid, ex); } } else { invokeErrorCallback(message.cbid, `方法不存在:${message.cmd}`); } } // 处理回调 else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } }); }; /** * @description 获取 mac 地址 * @returns */ export const getMac = () => { return request<string>({ cmd: "getMac", }); };
下面是主进程的代码封装:
import { ipcMain } from "electron"; import handle from "./handle"; /* eslint-disable no-shadow */ const callbacks: { [propName: string]: (data: unknown) => void } = {}; const errorCallbacks: { [propName: string]: (data: unknown) => void } = {}; function postMessage( webContents: Electron.WebContents, data: { cmd: string; data?: unknown }, cb?: (data: unknown) => void, errorCb?: (data: unknown) => void, ) { if (cb) { const cbid = Date.now().toString(); callbacks[cbid] = cb; webContents.send("message", { cmd: data.cmd, data: data.data, cbid: cbid, }); if (errorCb) { errorCallbacks[cbid] = errorCb; } } else { webContents.send("message", { cmd: data.cmd, data: data.data, }); } } function request<T = unknown>( webContents: Electron.WebContents, params: { cmd: string; data?: unknown }, ) { return new Promise<T>((resolve, reject) => { postMessage( webContents, { cmd: params.cmd, data: params.data }, (res) => { resolve(res as T); }, (error) => { reject(error); }, ); }); } function invokeCallback<T = unknown>( webContents: Electron.WebContents, cbid: string, res: T, ) { webContents.send("message", { cmd: "postMessageCallback", cbid, data: res, code: 200, }); } function invokeErrorCallback( webContents: Electron.WebContents, cbid: string, res: unknown, ) { webContents.send("message", { cmd: "postMessageCallback", cbid, data: res, code: 400, }); } export const addIpcMainEventListener = () => { ipcMain.on( "message", async ( event, message: { cmd: string; cbid: string; data: unknown; code?: number }, ) => { // 处理渲染进程主动发的消息 if (message.cmd !== "postMessageCallback") { if (handle[message.cmd]) { try { const res = await handle[message.cmd](event, message.data); invokeCallback(event.sender, message.cbid, res); } catch (ex: unknown) { invokeErrorCallback(event.sender, message.cbid, ex); } } else { invokeErrorCallback( event.sender, message.cbid, `方法不存在:${message.cmd}`, ); } } // 处理发出去的请求的回调 else { if (message.code === 200) { (callbacks[message.cbid] || function () {})(message.data); } else { (errorCallbacks[message.cbid] || function () {})(message.data); } delete callbacks[message.cbid]; // 执行完回调删除 delete errorCallbacks[message.cbid]; // 执行完回调删除 } }, ); }; // 主进程向 ipcRenderer 发起关闭请求,ipcRenderer 弹框确认,ipcRenderer 再通知主进程关闭窗口 export const closeWindow = (webContents: Electron.WebContents) => { return request(webContents, { cmd: "closeWindow", }); };
主进程处理消息:
import getMAC from "../getmac"; const handle: Record< string, // eslint-disable-next-line @typescript-eslint/no-explicit-any (event: Electron.IpcMainEvent, data: any) => void > = { getMac: () => { let mac = ""; try { mac = getMAC(); } catch (ex) { console.log(ex); } return mac; }, }; export default handle;
封装完之后,进程间的通讯就像发起 HTTP 请求一样简单
const handleGetMac = () => { getMac().then((mac) => { model.mac.value = mac; }); };
到此这篇关于一文带你搞懂Electron如何优雅的进行进程间通讯的文章就介绍到这了,更多相关Electron进程间通讯内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!