一文帶你搞懂Electron如何優(yōu)雅的進(jìn)行進(jìn)程間通訊
Electron 本身提供的進(jìn)程間通訊的方法比較偏底層,使用起來(lái)有些繁瑣、不易維護(hù)。Electron 中渲染進(jìn)程和主進(jìn)程之間的通訊,其實(shí)就像傳統(tǒng) web 開(kāi)發(fā)中網(wǎng)頁(yè)端與服務(wù)器的通訊。那么 Electron 中進(jìn)程間通訊能不能實(shí)現(xiàn)像調(diào)用 HTTP 請(qǐng)求一樣方便呢,答案是肯定的。
以下是一個(gè) HTTP 請(qǐng)求的簡(jiǎn)單封裝:
import { request } from "@/utils/request";
const { VITE_HOST } = import.meta.env;
// #region 操作密碼校驗(yàn)接口
export interface ICheckPwdResult {
/**
* 0:成功,其他:失敗
*/
code: string;
message: string;
}
export interface ICheckPwdData {
/**
* 類(lèi)型:LOCK_SCREEN:鎖屏
*/
type: string;
/**
* 密碼
*/
password: string;
}
/**
* 操作密碼校驗(yàn)接口
* /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 中渲染進(jìn)程向主進(jìn)程發(fā)送請(qǐng)求的封裝:
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",
});
};
下面細(xì)說(shuō)如何封裝:
Electron 進(jìn)程間通訊有四種模式:
- 渲染器進(jìn)程到主進(jìn)程(單向)
- 渲染器進(jìn)程到主進(jìn)程(雙向)
- 主進(jìn)程到渲染器進(jìn)程(單向)
- 渲染器進(jìn)程到渲染器進(jìn)程
更多細(xì)節(jié)可以查看 進(jìn)程間通信 | Electron
單向和雙向的區(qū)別就是:?jiǎn)蜗虻南l(fā)出去之后是沒(méi)有消息返回的,或者說(shuō)拿不到返回的消息;雙向就是請(qǐng)求發(fā)出后可以拿到響應(yīng)信息,就跟 HTTP 請(qǐng)求一樣。
舉個(gè)例子,頁(yè)面向主進(jìn)程發(fā)起請(qǐng)求,希望拿到計(jì)算機(jī)的 mac 地址,主進(jìn)程通過(guò)調(diào)用相應(yīng)的 nodejs api 拿到 mac 地址返回,頁(yè)面拿到消息結(jié)果,這就是雙向的。
我并沒(méi)有用到原生就支持雙向的 2 模式,而是使用都是單向的 1、3 模式,通過(guò)封裝后實(shí)現(xiàn)雙向的效果。主要是因?yàn)?Electron 并不支持主進(jìn)程到渲染器進(jìn)程的雙向模式,也就是主進(jìn)程給頁(yè)面主動(dòng)發(fā)送消息后是無(wú)法拿到響應(yīng)的。為了保持封裝后代碼的統(tǒng)一性,都使用了單向的模式進(jìn)行封裝。
結(jié)合下圖以及實(shí)際代碼調(diào)用,說(shuō)說(shuō)是如何基于單向的消息模式實(shí)現(xiàn)雙向通訊的。

/**
* @description 獲取 mac 地址
* @returns
*/
export const getMac = () => {
return request<string>({
cmd: "getMac",
});
};
const handleGetMac = () => {
getMac().then((mac) => {
model.mac.value = mac;
});
};
頁(yè)面中通過(guò)調(diào)用getMac方法獲取計(jì)算機(jī) mac 地址。
getMac方法是一個(gè)異步方法,渲染進(jìn)程在調(diào)用這個(gè)方法發(fā)送消息的時(shí)候,會(huì)生成一個(gè) uuid,然后將 uuid 、cmd 字段、data 數(shù)據(jù)(這里沒(méi)有)放到消息體里再發(fā)送給主進(jìn)程。同時(shí)會(huì)在回調(diào)隊(duì)列里增加兩條回調(diào),一條成功回調(diào),一條失敗回調(diào),并且使用 uuid 標(biāo)識(shí),成功回調(diào)就是 then 里面的函數(shù),失敗回調(diào)就是 catch 里的函數(shù)。
主進(jìn)程接收到消息后,根據(jù)cmd 字段做相應(yīng)的處理,處理完后將結(jié)果和渲染進(jìn)程發(fā)送的消息體里的 uuid 放到響應(yīng)消息體里,響應(yīng)消息體里也有 cmd 字段,并且是固定(我使用postMessageCallback標(biāo)識(shí)),這樣渲染進(jìn)程在接收到消息的時(shí)候就知道這是一條之前發(fā)送出去的消息的響應(yīng)。響應(yīng)消息體里還有一個(gè)code 字段標(biāo)識(shí)是成功還是失敗。
渲染進(jìn)程接收到消息后,根據(jù)消息體里的 cmd 字段判斷消息是響應(yīng)消息(cmd為postMessageCallback)還是普通消息(主進(jìn)程主動(dòng)發(fā)送的消息)。如果是響應(yīng)消息,根據(jù) uuid 在回調(diào)隊(duì)列里找到相應(yīng)的回調(diào),再根據(jù) code 判斷是執(zhí)行成功回調(diào)還是失敗回調(diào)。
以上就是渲染進(jìn)程到主進(jìn)程的單向消息模式實(shí)現(xiàn)雙向通訊的整個(gè)通訊過(guò)程。把消息的發(fā)送、處理、返回響應(yīng)的對(duì)象逆過(guò)來(lái)就是主進(jìn)程到渲染進(jìn)程的雙向通訊了。
原理說(shuō)清楚了,下面就是代碼實(shí)現(xiàn)了。
渲染進(jìn)程需要有發(fā)送消息、監(jiān)聽(tīng)消息的功能:
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);
},
這樣頁(yè)面中 window 對(duì)象里就有了 ipcRenderer 對(duì)象,ipcRenderer 對(duì)象提供了 addEventListener、postMessage 方法,分別用來(lái)監(jiān)聽(tīng)消息和發(fā)送消息。
把 addEventListener、postMessage 再做一下封裝,就可以像調(diào)用 HTTP 請(qǐng)求一樣向主進(jìn)程發(fā)起請(qǐng)求了。
/* 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);
// 處理主進(jìn)程主動(dòng)發(fā)的消息
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}`);
}
}
// 處理回調(diào)
else {
if (message.code === 200) {
(callbacks[message.cbid] || function () {})(message.data);
} else {
(errorCallbacks[message.cbid] || function () {})(message.data);
}
delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除
delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除
}
});
};
/**
* @description 獲取 mac 地址
* @returns
*/
export const getMac = () => {
return request<string>({
cmd: "getMac",
});
};
下面是主進(jìn)程的代碼封裝:
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 },
) => {
// 處理渲染進(jìn)程主動(dòng)發(fā)的消息
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}`,
);
}
}
// 處理發(fā)出去的請(qǐng)求的回調(diào)
else {
if (message.code === 200) {
(callbacks[message.cbid] || function () {})(message.data);
} else {
(errorCallbacks[message.cbid] || function () {})(message.data);
}
delete callbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除
delete errorCallbacks[message.cbid]; // 執(zhí)行完回調(diào)刪除
}
},
);
};
// 主進(jìn)程向 ipcRenderer 發(fā)起關(guān)閉請(qǐng)求,ipcRenderer 彈框確認(rèn),ipcRenderer 再通知主進(jìn)程關(guān)閉窗口
export const closeWindow = (webContents: Electron.WebContents) => {
return request(webContents, {
cmd: "closeWindow",
});
};
主進(jìn)程處理消息:
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;
封裝完之后,進(jìn)程間的通訊就像發(fā)起 HTTP 請(qǐng)求一樣簡(jiǎn)單
const handleGetMac = () => {
getMac().then((mac) => {
model.mac.value = mac;
});
};

到此這篇關(guān)于一文帶你搞懂Electron如何優(yōu)雅的進(jìn)行進(jìn)程間通訊的文章就介紹到這了,更多相關(guān)Electron進(jìn)程間通訊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
實(shí)例講解javascript注冊(cè)事件處理函數(shù)
這篇文章主要以實(shí)例的方式向大家介紹了javascript注冊(cè)事件處理函數(shù),內(nèi)容很全面,感興趣的朋友可以參考一下2016-01-01
uni-app自定義導(dǎo)航欄右側(cè)做增加按鈕并跳轉(zhuǎn)鏈接功能
這篇文章主要介紹了uni-app自定義導(dǎo)航欄右側(cè)做增加按鈕并跳轉(zhuǎn)鏈接,本文通過(guò)實(shí)例代碼給大家分享實(shí)現(xiàn)思路,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
javascript+html5實(shí)現(xiàn)繪制圓環(huán)的方法
這篇文章主要介紹了javascript+html5實(shí)現(xiàn)繪制圓環(huán)的方法,實(shí)例分析了javascript實(shí)現(xiàn)html5基于canvas繪制圓環(huán)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
JS數(shù)組實(shí)現(xiàn)分類(lèi)統(tǒng)計(jì)實(shí)例代碼
本文通過(guò)實(shí)例代碼給大家介紹了js數(shù)組實(shí)現(xiàn)分類(lèi)統(tǒng)計(jì)的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09
javascript 數(shù)組去重復(fù)(在線(xiàn)去重工具)
很多情況下我們需要去掉重復(fù)的內(nèi)容,一般我們都是將很多內(nèi)容放到一個(gè)數(shù)組里面,然后再去重復(fù),這里簡(jiǎn)單為大家整理一下2016-12-12
瀏覽器JavaScript調(diào)試功能無(wú)法使用解決方案
這篇文章主要介紹了瀏覽器JavaScript調(diào)試功能無(wú)法使用解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09

