一文带你搞懂Electron如何优雅的进行进程间通讯

 更新时间:2024年11月20日 09:49:30   作者:若邪  
这篇文章主要为大家详细介绍了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 字段判断消息是响应消息(cmdpostMessageCallback)还是普通消息(主进程主动发送的消息)。如果是响应消息,根据 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 对象提供了 addEventListenerpostMessage 方法,分别用来监听消息和发送消息。

addEventListenerpostMessage 再做一下封装,就可以像调用 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进程间通讯内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解JWT token心得与使用实例

    详解JWT token心得与使用实例

    这篇文章主要介绍了详解JWT token心得与使用实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • 详解CocosCreator游戏之鱼群算法

    详解CocosCreator游戏之鱼群算法

    这篇文章主要介绍了详解CocosCreator游戏之鱼群算法,对算法感兴趣的同学,可以仔细参考下
    2021-04-04
  • javascript 获取模态窗口的滚动位置代码

    javascript 获取模态窗口的滚动位置代码

    模态窗口的滚动位置获取办法还有不知道的吗?下面的方法或许对大家有所帮助,感兴趣的朋友可以了解下,希望对大家有所帮助
    2013-08-08
  • js去除输入框中所有的空格和禁止输入空格的方法

    js去除输入框中所有的空格和禁止输入空格的方法

    这篇文章主要介绍当输入框输入数值的时候使用js去除所有的空格和禁止输入空格的方法,需要的朋友可以参考下
    2014-06-06
  • 开源免费天气预报接口API及全国所有地区代码(国家气象局提供)

    开源免费天气预报接口API及全国所有地区代码(国家气象局提供)

    这篇文章主要介绍了开源免费天气预报接口API及全国所有地区代码(国家气象局提供)的相关资料,需要的朋友可以参考下
    2016-12-12
  • JS把数据添加到列表中的几种常见方法

    JS把数据添加到列表中的几种常见方法

    在JavaScript中,向列表数组添加数据是一项基础且高频的操作,无论是动态生成内容、处理用户输入还是与后端API交互,掌握数组操作方法都是前端开发者的必备技能,本文将详细介绍几种常用的添加数据到数组的方法,需要的朋友可以参考下
    2026-03-03
  • JavaScript数据结构之数组的表示方法示例

    JavaScript数据结构之数组的表示方法示例

    这篇文章主要介绍了JavaScript数据结构之数组的表示方法,从数据结构线性表的角度分析了数组的原理并结合实例形式分析了javascript数组的定义与使用方法,需要的朋友可以参考下
    2017-04-04
  • JavaScript中代理与反射的用法详解

    JavaScript中代理与反射的用法详解

    JavaScript作为一门灵活而强大的语言,提供了代理(Proxy)与反射(Reflect)这两个元编程工具,它们为开发者提供了更深层次的语言控制和操作,在本篇博客中,我们将深入研究代理与反射的概念、用法,以及如何巧妙地结合它们来实现高级的编程技巧,需要的朋友可以参考下
    2023-12-12
  • javascript 根据歌名获取播放地址和歌词内容

    javascript 根据歌名获取播放地址和歌词内容

    在前几天做在线听歌的过程中,碰到了根据歌名获取播放地址和LRC文件内容的问题,今晚花了几个小时把接口整理了一下
    2009-06-06
  • js实现控制文件拖拽并获取拖拽内容功能

    js实现控制文件拖拽并获取拖拽内容功能

    本片文章主要给大家分享了用JS写出控制文件拖拽并获取拖拽内容功能实现过程,以及代码分享,有兴趣的一起学习下。
    2018-02-02

最新评论