实现electron与grpc通信,并解决打包后preload路径等问题

517 阅读4分钟

核心思想:

  1. electron通过主进程node模块调用grpc
  2. 借助preload预加载使用ipc通信主进程调用的grpc
  3. 渲染进程调用preload预加载脚本暴露的函数实现通信

实现grpc引入:

  1. 引入生成的grpc客户端代码,并安装指定依赖:

    npm i grpc google-protobuf @grpc/proto-loader
    
  2. 构造grpc代码调用

    /**
     * [ grpcClient.js ]
     * GRPC接口定义,待主进程调用之后与preload预加载脚本通信
     */
    
    const grpc = require('grpc');
    const { FileUploadServiceClient } = require('../grpc/Upload_grpc_pb'); //gRPC 客户端文件路径
    
    const ip = '192.168.100.189:50051'
    
    //创建一个连接
    const createInsecure = grpc.credentials.createInsecure()
    
    /**
     * 创建 gRPC 客户端实例并指定 gRPC 服务端点和凭据
     */
    function createGRPCClientSet() {
      const grpcClient = new FileUploadServiceClient(ip, createInsecure)
      return grpcClient
    }
    
    // 调用grpc接口getUploadList
    function getUploadList(request) {
      return new Promise((resolve, reject) => {
        const grpcClient = createGRPCClientGet();
        grpcClient.getUploadList(request, (error, response) => {
          if (error) {
            reject(error); // 如果发生错误,拒绝 Promise
          } else {
            resolve(response); // 如果成功,解决 Promise
          }
        });
      });
    }
    module.exports = {
        getUploadList
    }
    
  3. 主进程调用grpcClient.js文件定义的grpc调用函数待后续与preload通信

    async function createWindow() {
        
      // 在初始化窗口的时候,通过 IPC 通信触发 Node.js 服务的操作
      require('./isMain/GrpcToBackground')
    
      const win = new BrowserWindow({
        width: 1120,
        height: 680,
    
        webPreferences: {
    
          preload: path.join(__dirname, 'preload.js'),
          
          nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
          contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
        },
    
        autoHideMenuBar: false, // 隐藏顶部工具栏,生产环境时设置为true
        // frame: false // 无边框
      })
    
      if (process.env.WEBPACK_DEV_SERVER_URL) {
        await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
        if (!process.env.IS_TEST) win.webContents.openDevTools()
      } else {
        createProtocol('app')
        win.loadURL('app://./index.html')
      }
    }
    

    在此将主进程node调用grpc这一步封装来优化代码, require('./isMain/GrpcToBackground')文件中实现

  4. GrpcToBackground.js

    /**
     * [ GrpcToBackground.js ]
     * 在background主进程中调用grpc代码, 待preload预加载脚本通信
     */
    import { ipcMain } from 'electron';
    var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js')
    var google_protobuf_wrappers_pb = require('google-protobuf/google/protobuf/wrappers_pb.js');
    const { NewUploadItem, UploadItemTransInfo, PauseUploadInfo, PauseMethodMsg, TransSetting } = require('../grpc/Upload_pb')
    // 导入 Node.js 服务代码
    const grpcService = require('../api/grpcClient')
    
    // 获取上传列表(grpc)
    ipcMain.handle('getUploadList', async () => {
      try {
        const request = new google_protobuf_empty_pb.Empty()
        const response = await grpcService.getUploadList(request)
        return ListArray(response)
      } catch (error) {
        console.log('error', error)
      }
    })
    
  5. 此时在主进程中调用了grpc可能会出现问题,

    1. 找不到grpc依赖下的electron-v93-win32-x64-unkonwn/grpc_node.node文件 image-20230919123508426.png
    2. 这里我将同级的node-v93-win32-x64-unknown/grpc_node.node拷贝到electron-v93-win32-x64-unkonwn下出现的问题 image-20230919123542280.png
  6. 解决: 解决electronnode兼容性问题

    1. 尝试找到electronnode相同版本的node_module_version,发现只有10.xxx比较老版本,所以无法使用对应更老版本的nodeelectron

    2. 所以根据electron官网提供的编译工具(@electron/rebuild)将重建electronnode适配版本,解决问题

    3. 具体操作:

      1. 安装:npm install --save-dev @electron/rebuild

      2. 看见package.json中多了rebuild image-20230922094309042.png

      3. 使用npm run rebuild编译

      4. 可能遇到的问题:包含gyp或者node-gyp的报错信息

        解决: 需要安装python v3.11.5下载

        需要安装 Python 的原因是因为 Electron 使用了 Node.js 的 C++ 模块,而这些模块需要编译成二进制代码以在不同的操作系统上运行。Python 在这里的作用是用于构建这些 C++ 模块,因为在构建过程中可能会涉及到一些编译任务
        
      5. 重新运行npm run rebuild编译

preload通信grpc:

  1. 定义preload文件并使用绝对路径引入到主进程中

    async function createWindow() {
        
      // 在初始化窗口的时候,通过 IPC 通信触发 Node.js 服务的操作
      require('./isMain/GrpcToBackground')
    
      const win = new BrowserWindow({
        width: 1120,
        height: 680,
    
        webPreferences: {
    
          preload: path.join(__dirname, 'preload.js'),
          
          nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
          contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
        },
    
        autoHideMenuBar: false, // 隐藏顶部工具栏,生产环境时设置为true
        // frame: false // 无边框
      })
    }
    

    可能会出现的问题: 在打包过后,找不到preload文件,所以需要额外打包配置(vue.config.js)

    module.exports = defineConfig({
      pluginOptions: {
        electronBuilder: {
          preload: 'src/preload/index.js' //指定preload路径
        }
      }
    })
    
  2. preload中,实现通信grpc

    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld('versions', {
      node: 'electron',
      /**
       * GRPC 
       */
      // 获取上传文件列表
      getUploadListGRPC: () => {
        return ipcRenderer.invoke('getUploadList')
      }
    })
    
  3. 此时完成:

    1. 完成了grpc的引入
    2. 完成定义的grpc在主进程node中调用
    3. 使用preload通信主进程中调用的grpc

渲染进程调用:

vue页面中调用preload暴露的函数,使用window.versions.getUploadListGRPC()

/**
 * [ uploading.vue ]
 */
<!-- 正在上传页面 -->
<template>
  <div></div>
</template>

<script>
export default {
  name: 'downLoading',
  data() {
    return {
      InterTimer: null
    }
  },
  methods: {
    getList() {
      clearInterval(this.InterTimer)
      const updateList = async () => {
        // 通过调用preload来实际通信grpc
        const res = await window.versions.getUploadListGRPC();
        this.fileList = [];
        res && res.forEach(item => {
          if (item.parentID === "") {
            this.fileList.push(item);
          } else {
            const parent = res.find(parentItem => parentItem.itemID === item.parentID);
            if (parent) {
              if (!parent.children) parent.children = [];
              parent.children.push(item);
            }
          }
        });
        if (this.fileList.length === 0) return clearInterval(this.InterTimer)
      };

      // 立即执行一次
      updateList();

      // 然后每隔1秒执行一次
      this.InterTimer = setInterval(updateList, 1000);
    },
  },
  created() {
    this.getList()
  },
}
</script>

此时实现了在渲染进程页面中去调用grpc实现通信

grpc类型转换

当调用grpc传参,获取参数的时候都需要转换参数类型:

  1. 根据.proto文件定义的消息类型

  2. 在生成的消息类型js文件中可以查看具体如何转换参数类型

  3. 例如:

    // grpc
    getUploadList: {
       path: '/StateService/GetUploadList',
       requestStream: false,
       responseStream: false,
       requestType: google_protobuf_empty_pb.Empty, // 传参类型
       responseType: Upload_pb.UploadList,
       requestSerialize: serialize_google_protobuf_Empty,
       requestDeserialize: deserialize_google_protobuf_Empty,
       responseSerialize: serialize_UploadList,
       responseDeserialize: deserialize_UploadList,
    }
    
    // 类型转换
    var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js')
    
    ipcMain.handle('getUploadList', async () => {
      try {
        const request = new google_protobuf_empty_pb.Empty() //类型转换为grpc定义入参类型
        const response = await grpcService.getUploadList(request)
        return ListArray(response)
      } catch (error) {
        console.log('error', error)
      }
    })
    

至于为什么要response.toObject()toObject():是从grpc的消息类型的文件中得知,根据定义的response的类型为Upload_pb.UploadList,因此可以在类型文件中的UploadList原型上去查看他的方法,转为我们客户端可以展示的数据格式。