一文帶你搞懂Electron如何優(yōu)雅的進(jìn)行進(jìn)程間通訊
Electron 本身提供的進(jìn)程間通訊的方法比較偏底層,使用起來有些繁瑣、不易維護(hù)。Electron 中渲染進(jìn)程和主進(jìn)程之間的通訊,其實(shí)就像傳統(tǒng) web 開發(fā)中網(wǎng)頁端與服務(wù)器的通訊。那么 Electron 中進(jìn)程間通訊能不能實(shí)現(xiàn)像調(diào)用 HTTP 請求一樣方便呢,答案是肯定的。
以下是一個(gè) HTTP 請求的簡單封裝:
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 {
/**
* 類型: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ā)送請求的封裝:
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ì)說如何封裝:
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ū)別就是:單向的消息發(fā)出去之后是沒有消息返回的,或者說拿不到返回的消息;雙向就是請求發(fā)出后可以拿到響應(yīng)信息,就跟 HTTP 請求一樣。
舉個(gè)例子,頁面向主進(jìn)程發(fā)起請求,希望拿到計(jì)算機(jī)的 mac 地址,主進(jìn)程通過調(diào)用相應(yīng)的 nodejs api 拿到 mac 地址返回,頁面拿到消息結(jié)果,這就是雙向的。
我并沒有用到原生就支持雙向的 2 模式,而是使用都是單向的 1、3 模式,通過封裝后實(shí)現(xiàn)雙向的效果。主要是因?yàn)?Electron 并不支持主進(jìn)程到渲染器進(jìn)程的雙向模式,也就是主進(jìn)程給頁面主動(dòng)發(fā)送消息后是無法拿到響應(yīng)的。為了保持封裝后代碼的統(tǒng)一性,都使用了單向的模式進(jìn)行封裝。
結(jié)合下圖以及實(shí)際代碼調(diào)用,說說是如何基于單向的消息模式實(shí)現(xiàn)雙向通訊的。

/**
* @description 獲取 mac 地址
* @returns
*/
export const getMac = () => {
return request<string>({
cmd: "getMac",
});
};
const handleGetMac = () => {
getMac().then((mac) => {
model.mac.value = mac;
});
};
頁面中通過調(diào)用getMac方法獲取計(jì)算機(jī) mac 地址。
getMac方法是一個(gè)異步方法,渲染進(jìn)程在調(diào)用這個(gè)方法發(fā)送消息的時(shí)候,會生成一個(gè) uuid,然后將 uuid 、cmd 字段、data 數(shù)據(jù)(這里沒有)放到消息體里再發(fā)送給主進(jìn)程。同時(shí)會在回調(diào)隊(duì)列里增加兩條回調(diào),一條成功回調(diào),一條失敗回調(diào),并且使用 uuid 標(biāo)識,成功回調(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)識),這樣渲染進(jìn)程在接收到消息的時(shí)候就知道這是一條之前發(fā)送出去的消息的響應(yīng)。響應(yīng)消息體里還有一個(gè)code 字段標(biāo)識是成功還是失敗。
渲染進(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è)通訊過程。把消息的發(fā)送、處理、返回響應(yīng)的對象逆過來就是主進(jìn)程到渲染進(jìn)程的雙向通訊了。
原理說清楚了,下面就是代碼實(shí)現(xiàn)了。
渲染進(jìn)程需要有發(fā)送消息、監(jiān)聽消息的功能:
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 方法,分別用來監(jiān)聽消息和發(fā)送消息。
把 addEventListener、postMessage 再做一下封裝,就可以像調(diào)用 HTTP 請求一樣向主進(jìn)程發(fā)起請求了。
/* 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ā)出去的請求的回調(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)閉請求,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 請求一樣簡單
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript 獲取模態(tài)窗口的滾動(dòng)位置代碼
模態(tài)窗口的滾動(dòng)位置獲取辦法還有不知道的嗎?下面的方法或許對大家有所幫助,感興趣的朋友可以了解下,希望對大家有所幫助2013-08-08
開源免費(fèi)天氣預(yù)報(bào)接口API及全國所有地區(qū)代碼(國家氣象局提供)
這篇文章主要介紹了開源免費(fèi)天氣預(yù)報(bào)接口API及全國所有地區(qū)代碼(國家氣象局提供)的相關(guān)資料,需要的朋友可以參考下2016-12-12
JavaScript數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組的表示方法示例
這篇文章主要介紹了JavaScript數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組的表示方法,從數(shù)據(jù)結(jié)構(gòu)線性表的角度分析了數(shù)組的原理并結(jié)合實(shí)例形式分析了javascript數(shù)組的定義與使用方法,需要的朋友可以參考下2017-04-04
javascript 根據(jù)歌名獲取播放地址和歌詞內(nèi)容
在前幾天做在線聽歌的過程中,碰到了根據(jù)歌名獲取播放地址和LRC文件內(nèi)容的問題,今晚花了幾個(gè)小時(shí)把接口整理了一下2009-06-06
js實(shí)現(xiàn)控制文件拖拽并獲取拖拽內(nèi)容功能
本片文章主要給大家分享了用JS寫出控制文件拖拽并獲取拖拽內(nèi)容功能實(shí)現(xiàn)過程,以及代碼分享,有興趣的一起學(xué)習(xí)下。2018-02-02

