Electron(1):主、渲染进程通信的封装处理

1,140 阅读2分钟

前言

Electron进程通信主要有主 -> 渲染进程, 渲染 -> 主进程, 渲染进程 -> 渲染进程,本文主要从可用性,便捷性,通用型来处理通信;

你将收获如下:

  • ipcMain和ipcRenderer的基本使用
  • 通信类的封装

先说一下场景

一个项目有很多跟桌面交互的场景,比如打开文件、选择文件(文件夹);

electron提供给我们的ipc是ipcRendereripcMain,拿选择文件夹举例,我的流程应该是

  • ipcMain监听channel名称为selectFolder的请求
  • ipcRenderer发送一个channelselectFolder给主进程
  • 主进程监听到了这个事件后,进行dialog.showOpenDialogSync,然后返回给渲染进程

以上流程可以实现功能,但是有一些小问题

  • 比如多个页面都要选择文件夹,要写多套ipcRenderer.send('selectFolder'),重复不好维护,万一哪天ipcMain改了监听的channel名字,所有渲染进程的selectFolder都得跟着改
  • 好像没其他的了,写不下去了

根据上面的流程,我们分析了很多类似的操作都可以放到一个类里面去维护,比如选择文件、选择文件夹、打开文件这类属于system类型的操作,可以放到一个systemIPC类;请求接口等可以放到一个networkIPC类中;

编写我们的IPC类

由于存在主进程和渲染器进程,所以对应都要写一个基础类,然后在基础类上进行定义业务所需要的方法;

主进程的IPC类,主要实现了一个IpcMainBaseController基础类,以及SystemController

class IpcMainBaseController {
  prefix: string
  constructor(prefix: string) {
    this.prefix = prefix
  }

  getChannelName(name: string) {
    return [this.prefix, name].join(':')
  }
}

import type { IpcMainEvent } from 'electron'
import { BrowserWindow, screen, desktopCapturer, clipboard, nativeImage } from 'electron'


export class SystemController extends IpcMainBaseController {
  constructor() {
    super('system')
  }

  async closeWindow(event: IpcMainEvent) {
    return event.sender.close()
  }

  async hideWindow(event: IpcMainEvent) {
    const window = BrowserWindow.fromWebContents(event.sender)
    window?.hide()
  }

  async showWindow(event: IpcMainEvent) {
    const window = BrowserWindow.fromWebContents(event.sender)
    window?.show()
  }
 
  async getScreenSize() {
    const primaryDisplay = screen.getPrimaryDisplay()
    const { width, height } = primaryDisplay.workAreaSize
    return {
      width,
      height
    }
  }
}

渲染进程的IPC类,同样编写一个基础类IpcRendererService还有业务类SystemIpcRendererService

import type { IpcRenderer } from 'electron'
export class IpcRendererService {
  prefix: string
  ipcRenderer: IpcRenderer
  constructor(prefix: string) {
    this.prefix = prefix
    this.ipcRenderer = (window.electron.ipcRenderer || window.ipcRenderer) as unknown as IpcRenderer
  }

  async invoke(channel: string, params?: Object): Promise<any> {
    try {
      const fullChannel = `${this.prefix}:${channel}`
      this.initCallbackChannel(channel, options)
      const result = await this.ipcRenderer.invoke(
        fullChannel,
        params || {},
      )
      return result
    } catch (error) {
      console.error('[invoke error]: ', error)
      throw error
    }
  }
}

export class SystemIpcRendererService extends IpcRendererService {
  constructor() {
    super('system')
  }

  async closeWindow() {
    return await this.invoke('closeWindow')
  }

  async showWindow() {
    return await this.invoke('showWindow')
  }

  async hideWindow() {
    return await this.invoke('hideWindow')
  }

  async getScreenSize() {
    return this.invoke('getScreenSize')
  }

  
}

export const systemIpcRendererService = new SystemIpcRendererService()


主进程进行channel监听

import { ipcMain } from 'electron'
import type { BrowserWindow } from 'electron'
import { IpcMainBaseController } from './base'
import { SystemController } from './system'

export const enumControllerMethods = <T extends IpcMainBaseController>(clsInstance: T) => {
  const result = {}
  const filterKeys = ['constructor']
  const keys = Object.getOwnPropertyNames(clsInstance.constructor.prototype)
  keys.forEach((key) => {
    if (filterKeys.includes(key)) {
      return
    }
    const serviceFunction = clsInstance[key]
    if (typeof serviceFunction === 'function') {
      const channel = clsInstance.getChannelName(key)
      // 这段是关键,通过getChannelName拿到需要监听的channel,进行事件绑定
      ipcMain.handle(channel, serviceFunction.bind(clsInstance))
      result[channel] = serviceFunction
    }
  })

  return result
}

export const registerMainHanlders = (mainWindow: BrowserWindow) => {
  const system = new SystemController()

  enumControllerMethods(system)
}

使用(vue3)

<script setup lang="ts">
import { reactive } from 'vue'
import { systemIpcRendererService } from '@/ipc/system'
    
const hideWindow = async (item) => {
    await systemIpcRendererService.hideWindow()
}

const showWindow = async (item) => {
    await systemIpcRendererService.showWindow()
}
</script>

<template>
  <div class="py-6">
     <div @click="hideWindow">隐藏window</div>
     <div @click="showWindow">显示window</div>
  </div>
</template>

<style lang="less" scoped></style>

最后

本文只是提供了一个简单的思路,方便项目进行channel的维护;github代码,完整示例请看https://github.com/rictt/electron-practices