头图

前言

最近使用Eelctron开发了一个桌面端软件,并对之前所学习的大文件切片上传,以及nest框架做一个实践

开源地址https://github.com/whwanyt/fen_im_pc

阅读须知:

  1. 代码使用TypeScript,vue-setup
  2. 脚手架 electron-vite
  3. 开发环境:window,node:v16.15.1,pnpm:v7.9.0

本文知识点

  • 使用electron ipc 进行渲染进程和主进程的通行
  • 主进程使用单例模式和Map对多窗口进行管理
  • 使用electron-updater进行软件更新

项目效果图

0342bdefd6e94734a8f3cd1af874a0a.png

项目搭建

  1. 拉取模板项目https://github.com/alex8088/electron-vite-boilerplate
  2. 执行项目初始化并运行

    npm install
    npm run dev

    效果如下

1665305936795.png

多窗口管理

在main目录下新建windows.ts文件,并实现窗口创建及管理的单例类


import { shell, BrowserWindow, ipcMain } from 'electron'
import { is } from '@electron-toolkit/utils'
import * as path from 'path'

export interface CreateWindowOptions {
  module: string //窗口模块名称
  center?: boolean //打开新页面时是否显示在屏幕中心
  url?: string //窗口链接
  width?: number 
  height?: number
  maximizable?: boolean  //是否可以最大化
}

export type winModule = {
  id: number
  url: string
}

export class WindowsMain {
  //key为winid,value为创建窗口返回的对象
  BrowserWindowsMap = new Map<number, BrowserWindow>()
  //key为窗口模块名称,方便通过模块名称查询
  winModulesMap = new Map<string, winModule>()
  constructor() {}

  static instance: WindowsMain

  static getInstance() {
    if (!this.instance) {
      this.instance = new WindowsMain()
    }
    return this.instance
  }

  
}

实现创建窗口方法

  newWindow(options: CreateWindowOptions): BrowserWindow {
    //通过创建窗口模块名称判断是否已经存在,存在就获取焦点,并将数据通过ipc通知到该窗口
    if (this.winModulesMap.has(options.module)) {
      const id = this.winModulesMap.get(options.module)!.id
      const win = this.BrowserWindowsMap.get(id)
      win!.focus()
      const params = getRequest(options.url || '')
      win!.webContents.send('uploadData', params)
      return win!
    }
    options.url = options.url || ''
    options.width = options.width || 990
    options.height = options.height || 570
    options.maximizable = options.maximizable != undefined ? options.maximizable : true
    const currentWindow = BrowserWindow.getFocusedWindow()
    let coord: { x: number | undefined; y: number | undefined } = { x: undefined, y: undefined }
    //如果已经有打开的窗口,并且新窗口不是居于屏幕中央,则相对于上一个窗口进行偏移
    if (currentWindow && !options.center) {
      const [currentWindowX, currentWindowY] = currentWindow.getPosition()
      coord.x = currentWindowX + 30
      coord.y = currentWindowY + 30
    }
    const mainWindow = new BrowserWindow({
      width: options.width,
      height: options.height,
      show: false,
      frame: false,
      ...coord,
      center: options.center,
      maximizable: options.maximizable,
      autoHideMenuBar: true,
      ...(process.platform === 'linux'
        ? {
            icon: path.join(__dirname, '../../build/icon.png')
          }
        : {}),
      webPreferences: {
        preload: path.join(__dirname, '../preload/index.js')
      }
    })

    mainWindow.on('close', () => {
      this.detWin(mainWindow.id)
    })

    mainWindow.on('ready-to-show', () => {
      console.log('ready-to-show')
      //在窗口刷新时将窗口信息发送到渲染进程,方便指定窗口交互
      mainWindow.webContents.send('setWinInfo', {
        winViewId: mainWindow.id,
        winViewModule: options.module
      })
      mainWindow.show()
    })

    mainWindow.webContents.setWindowOpenHandler((details) => {
      shell.openExternal(details.url)
      return { action: 'deny' }
    })
    //开发模式下拼接打开路由
    if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
      mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + options.url)
    } else {
    //打包后读取文件,并使用哈希打开指定路由
      mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'), {
        hash: options.url
      })
    }
    //将窗口信息存储到map
    this.BrowserWindowsMap.set(mainWindow.id, mainWindow)
    this.winModulesMap.set(options.module, { id: mainWindow.id, url: options.url || '' })
    return mainWindow
  }

实现获取窗口对象的方法

getWin(winId: number) {
  return this.BrowserWindowsMap.get(winId)
}

实现删除窗口方法

  detWin(winId: number) {
    const win = this.BrowserWindowsMap.get(winId)
    try {
      if (this.BrowserWindowsMap.size > 1) {
        let key = ''
        this.winModulesMap.forEach((item, k) => {
          if (item.id === winId) {
            key = k
          }
        })
        if (key !== '') {
          this.winModulesMap.delete(key)
        }
        this.BrowserWindowsMap.delete(winId)
      }
      win?.close()
    } catch (error) {}
  }

修改index.ts文件中的createWindow函数如下,即可打开默认主窗口

function createWindow(): void {
  // Create the browser window.
  const windowMain = WindowsMain.getInstance()
  const win = windowMain.newWindow({ module: 'app' })
}

IpcMain交互

在preload目录下新建ipc.ts文件

实现窗口最小化和关闭

备注:window.winViewId来源于创建窗口时主进程向渲染进程的ipc
//渲染进程
window.api.WindowAppQuit({ winViewId: window.winViewId })
//主进程
function WindowAppMinimize() {
  ipcMain.on('appMinimize', (_event, data: PreloadOptions) => {
    const win = WindowsMain.getInstance().getWin(data.winViewId)
    win && win.minimize()
  })
}
function WindowAppQuit() {
  ipcMain.on('appQuit', (_event, data: PreloadOptions) => {
    WindowsMain.getInstance().detWin(data.winViewId)
  })
}

实现窗口尺寸变更

function changWindowSize() {
  ipcMain.on('changWindowSize', (_event, data: PreloadSizeOptions) => {
    const win = WindowsMain.getInstance().getWin(data.winViewId)
    win && win.setSize(data.width, data.height)
  })
}

实现打开新窗口

//主进程
function openWin() {
  ipcMain.on('openWin', (_event, data: PreloadUrlOptions) => {
    WindowsMain.getInstance().newWindow(data)
  })
}
//渲染进程
window.api.openWin({
  module: 'friend',
  url: '#/friend',
  width: 500,
  height: 420,
  maximizable: false,
  center: true
})

项目打包

cannot unpack electron zip file, will be re-downloaded error=zip: not a vali

将electron-v17.4.11-win32-x64.zip下载,放到C:xxx\AppData\Local\electron\Cache\目录下,

打包时缺少nsis等都可以先下载,然后通过上述方法解决

软件升级

创建update.ts,并实现autoUpdater的方法

import { app, BrowserWindow, ipcMain } from 'electron'
import { autoUpdater } from 'electron-updater'

const message = {
  error: '检查更新出错',
  checking: '正在检查更新…',
  updateAva: '正在更新',
  updateNotAva: '已经是最新版本',
  downloadProgress: '正在下载...'
}

export const handleUpdate = (win: BrowserWindow) => {
  autoUpdater.autoDownload = false
  autoUpdater.setFeedURL('http://192.168.0.105:8080/')
  // 通过main进程发送事件给renderer进程,提示更新信息
  const sendUpdateMessage = (data) => {
    win.webContents.send('update-message', data)
  }
  autoUpdater.on('error', function (_e) {
    // 异常处理
    sendUpdateMessage({ cmd: 'error', message: message.error })
  })
  autoUpdater.on('checking-for-update', function () {
    // 校验
    sendUpdateMessage({ cmd: 'checking-for-update', message: message.checking })
  })
  autoUpdater.on('update-available', function (info) {
    //可用更新
    sendUpdateMessage({ cmd: 'update-available', message: message.updateAva, info })
  })
  autoUpdater.on('update-not-available', function (info) {
    // 更新失败
    sendUpdateMessage({ cmd: 'update-not-available', message: message.updateNotAva, info: info })
  })
  autoUpdater.on('download-progress', function (progressObj) {
    // 更新下载进度事件
    sendUpdateMessage({ cmd: 'downloadProgress', message: message.downloadProgress, progressObj })
  })
  autoUpdater.on(
    'update-downloaded',
    function (_event, _releaseNotes, _releaseName, _releaseDate, _updateUrl, _quitAndUpdate) {
      ipcMain.on('isUpdateNow', (_e, _arg) => {
        // 开始更新
        autoUpdater.quitAndInstall()
        app.quit()
        // callback()
      })
      sendUpdateMessage({ cmd: 'isUpdateNow', message: null })
    }
  )

  ipcMain.on('checkForUpdate', () => {
    // 执行自动更新检查
    autoUpdater.checkForUpdates()
  })

  ipcMain.on('downloadUpdate', () => {
    // 执行下载
    autoUpdater.downloadUpdate()
  })
}

在主进程main.ts中调用handleUpdate函数

function createWindow(): void {
  // Create the browser window.
  const windowMain = WindowsMain.getInstance()
  const win = windowMain.newWindow({ module: 'app' })
  ipc()
  //调用
  handleUpdate(win)
}

在渲染进程主界面实现升级组件,并触发主进程autoUpdater检查是否需要升级

const onUpdate = () => {
  //判断是否主窗口
  if (window.winViewModule === 'app') {
    //触发升级检测
    window.electron.ipcRenderer.send('checkForUpdate')
    //监听主进程发过来的更新消息
    window.electron.ipcRenderer.on('update-message', (_event, val) => {
      console.log(val)
      switch (val.cmd) {
        case 'update-available':
          showUpdateModal.value = true
          info.value.version = val.info.version
          info.value.description = val.info.description || ''
          break
        case 'downloadProgress':
          console.log('下载进度', val.progressObj)
        case 'isUpdateNow':
          isCompletes.value = true
          break
        default:
          break
      }
    })
  }
}
//触发下载
const onDownloadUpdate = () => {
  window.electron.ipcRenderer.send('downloadUpdate')
}
//重启安装方法
const onResetUpdate = () => {
  window.electron.ipcRenderer.send('isUpdateNow')
}

参考文档


裁晨
0 声望0 粉丝

路虽远,行则将至;书虽难,学则必成。山有峰顶,海有彼岸;漫漫长途,终有回转;余味苦涩,终有回甘。