【Electron】vue+electron应用自动更新的实现

652 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

一、前言

对于一个应用来说,版本更新是最基础的功能之一,不然咱也不能让用户每次想用最新版本都去下载,对吧。关于electron应用的版本更新,我在这里说一下,我做这块逻辑时候的一些问题以及方法思考。

二、步骤

electron应用的自动更新工具,这里我使用的是electron-updater

首先是安装依赖

npm install electron-updater
// or
yarn add electron-updater

之后我们创建一个更新文件,用于存放更新逻辑。在这里我们要引入autoUpdater更新插件,创建实例相关,在这里建立与渲染进程的通讯。

// ElectronUpdate.js

import { autoUpdater } from 'electron-updater'
import { ipcMain } from 'electron'
const fs = require('fs-extra')
const log = require('electron-log')
const path = require('path')

// 更新服务器地址,比如"http://**.**.**.**:8080/download/" 这里我使用了环境变量配置
const uploadUrl = process.env.VUE_APP_UPDATEURL 
let win = '' // 主进程窗口实例

// 检测更新时候的监听
export function updateHandle (wins) {
  win = wins
  const message = {
    error: '检查更新出错',
    checking: '正在检查更新……',
    updateAva: '检测到新版本,正在下载……',
    updateNotAva: '现在使用的就是最新版本,不用更新'
  }
  // 设置检测更新文件位置,**.yml文件就是校对更新信息
  autoUpdater.updateConfigPath = path.join(__dirname, '../app-update.yml')
  // 给自动更新实例设置更新地址
  autoUpdater.setFeedURL(uploadUrl)
  // 监听更新报错
  autoUpdater.on('error', (error) => {
    // 将报错信息存入日志
    log.warn('main-process=======', message.error)
    sendUpdateMessage(error)
  })
  // 监听正在检测更新
  autoUpdater.on('checking-for-update', () => {
    log.warn('main-process=======', message.checking)
    sendUpdateMessage(message.checking)
  })
  // 监听检测到有更新文件,则开始下载
  autoUpdater.on('update-available', (info) => {
    log.warn('main-process=======', message.updateAva)
    sendUpdateMessage(message.updateAva)
  })
  // 监听检测到当前是最新版本,无需更新
  autoUpdater.on('update-not-available', (info) => {
    log.warn('main-process=======', message.updateNotAva)
    sendUpdateMessage(message.updateNotAva)
  })

  // 监听更新下载进度事件
  autoUpdater.on('download-progress', (progressObj) => {
    // 通过主窗口实例发送给渲染进程,告知当前更新进度
    win.webContents.send('downloadProgress', progressObj)
  })
  // 监听当前最新版本包已经下载完成
  autoUpdater.on('update-downloaded', () => {
    // 告知渲染进程,已经下载完成,准备更新了
    win.webContents.send('isUpdateNow')
    log.warn('main-process=======', '开始更新')
    // 窗口设置为可关闭
    win.setClosable(true)
    // 这里是为了标识,告知主进程,此窗口可关闭而不是隐藏,下面细说
    global.sharedObject.willQuitApp = true
    // 退出当前应用,并重载(完成更新)
    autoUpdater.quitAndInstall()
  })
  // 监听--检测更新(等待渲染进程通知)
  ipcMain.on('checkForUpdate', () => {
    // 执行自动更新检查
    autoUpdater.checkForUpdates()
  })
}

// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage (text) {
  win.webContents.send('app-update-message', text)
}

之后再主进程里面引入这个文件。

// background.js

import { updateHandle } from './electron-config/ElectronUpdate'

// 当我们再点击应用的关闭按钮(红叉)时候,我们期望的并不是直接退出程序,
// 所以我们要通过event.preventDefault();来阻止默认事件,
// 之后操作将窗口隐藏,但是当我们要进行更新操作的时候,就需要让他恢复原来的功能,即退出应用。
// --win是当前窗口实例
win.on('close', (event) => {
    if (global.sharedObject.willQuitApp) { // 用于更新退出逻辑
      // 关闭所有未销毁的窗口
      BrowserWindow.getAllWindows().forEach(item => {
        if (!item.isDestroyed()) {
          item.destroy()
        }
      })
      win = null // 将窗口实例置空
      app.quit() // 执行退出程序
    } else { // 隐藏窗口
      event.preventDefault()
      win.hide()
      if (process.platform !== 'darwin') {
        win.setSkipTaskbar(true)
      }
    }
})

// 加载更新相关文件
updateHandle(win)

之后我们在渲染进程创建一个文件,用于展示更新内容以及更新操作按钮。这里处理触发更新事件,展示更新内容相关。

// ElectronUpdate.vue
<template>
  <div>
    更新内容以及更新下载进度请自行完成
  </div>
</template>
<script>
import { getVersionInfo } from '@/api/settings' // 接口获取最新版本后
import { version } from '../../package.json' // 获取当前应用版本号
const remote = window.require('electron').remote
export default {
  name: 'ElectronUpdate',
  data () {
    return {
      version: '',
      content: '',
      tips: '',
      downloadPercent: 100,
      onLine: true,
      warningTip: '',
      percent: 0, // 断网时的进度值
      isUpdate: false, // 是否正在更新
    }
  },
  methods: {
    // 初始值设置
    init () {
      this.isUpdate = false
      this.tips = ''
      this.warningTip = ''
      this.percent = 0
      this.downloadPercent = 100
    },

    // 获取更新日志列表
    getLogList (isUpdate) {
      this.clearEvent()
      this.ipcEvents()
      console.log('getLogList==========', isUpdate)
      this.init()
      // 通过接口过去当前最新的版本以及公告内容
      getVersionInfo({ electronversion: version }).then(res => {
         // 逻辑可自行填写,可通过checkUpdateState()这个方法获取是否最新版本
         // 当确定可以更新的时候直接调用doUpdate()
         this.doUpdate()
      })
    },

    /** 检查版本是否需要更新---- 检测版本是否需要更新 */
    checkUpdateState (onlineVersion) {
      let state = false
      if (version && onlineVersion && version.toString().includes('.') && onlineVersion.toString().includes('.')) {
        const locArr = version.toString().split('.')
        const lineArr = onlineVersion.toString().split('.')
        state = this.checkValue(lineArr, locArr, 0)
        console.log('checkUpdateState', locArr, lineArr)
      }
      console.log('checkUpdateState', state)
      return state
    },

    /** 循环比较值 */
    checkValue (arr, locArr, i) {
      let state = false
      try {
        if (locArr[i] && arr[i]) {
          if (Number(locArr[i]) < Number(arr[i])) {
            state = true
          } else if (Number(locArr[i]) === Number(arr[i])) {
            state = this.checkValue(arr, locArr, i + 1)
          }
        } else if (arr[i]) {
          state = true
        }
      } catch (e) {
        console.log('checkValue', e)
      }

      return state
    },

    // 网络状况监听回调
    alertOnlineStatus () {
      this.onLine = navigator.onLine
      this.warningTip = !navigator.onLine ? '检测到网络已断开,请检查网络。' : ''
      if (this.onLine) {
        setTimeout(() => {
          if (this.percent >= this.downloadPercent) {
            this.warningTip = '更新中断,请重试'
          }
        }, 12e3)
      } else {
        this.percent = this.downloadPercent
      }
    },

    // 清除监听
    clearEvent () {
      this.init()
      window.ipcRenderer.removeAllListeners(['app-update-message'])
      window.ipcRenderer.removeAllListeners(['downloadProgress'])
      window.ipcRenderer.removeAllListeners(['isUpdateNow'])
    },

    // 检查更新, isUpdate是否更新
    async startUpdate (isUpdate) {
      console.log('startUpdate', isUpdate, version)
      await this.getLogList(isUpdate)
    },

    // 重试更新
    retryUpdate () {
      this.startUpdate(true)
    },

    // 开始更新
    doUpdate () {
      // 通知主进程检测更新
      window.ipcRenderer.send('checkForUpdate')
    },

    // 更新过程监听创建
    ipcEvents () {
      // 监听主进程的message系统版本信息消息
      window.ipcRenderer.on('app-update-message', (event, text) => {
        console.log(`%c message ---- ${text}`, 'color:#0183ff')
        if (this.isUpdate) return false
        if (text && text.toString().indexOf('Error') > -1) {
          this.warningTip = '更新中断,请重试'
          if (text.toString().indexOf('net::ERR_CONNECTION_RESET') > -1) {
            // 当前网络不稳定,连接不到服务器,提示稍后再继续更新
            this.$Message.info('当前网络不稳定,请稍后重试')
          } else if (text.toString().indexOf('Cannot download') > -1) {
            // 正在上传包,点击更新时候,获取不到更新包
            this.$Message.info('系统维护中,请稍后更新')
          } else {
            this.$Modal.info({
              title: '提示',
              content: '应用有重要版本更新,请您重新下载并安装,给您带来不便,敬请谅解。',
              showCancel: false,
              onOk: () => {
                const downloadUrl = '下载地址'
                window.shell.openExternal(downloadUrl)
              }
            })
          }
          window.onkeydown = () => { window.event.returnValue = true }
          this.init()
        } else {
          this.$Message.destroy()
          this.$Message.info({ content: text, duration: 2 })
          if (text === '现在使用的就是最新版本,不用更新') {
            this.$emit('onUpdate', 1)
            window.onkeydown = () => { window.event.returnValue = true }
            window.ipcRenderer.send('doMenu', true)
            this.init()
            setTimeout(() => { this.tips = text }, 1500)
          } else {
            this.tips = text
            this.$emit('onUpdate', 2)
          }
        }
      })
      // 监听下载进度
      window.ipcRenderer.on('downloadProgress', (event, progressObj) => {
        window.onkeydown = () => { window.event.returnValue = false }
        this.downloadPercent = Number(progressObj.percent.toFixed(2)) || 0
        console.log(`%c downloadProgress ---- ${progressObj.percent}`, 'color:#0183ff')
      })
      // 监听开始更新
      window.ipcRenderer.on('isUpdateNow', () => {
        console.log('%c isUpdateNow', 'color:#0183ff')
        this.isUpdate = true
      })
    }
  },
  created () {
    this.init()
    this.ipcEvents()
    // 开启键盘响应
    window.onkeydown = () => { window.event.returnValue = true }
    // 检测网络是否正常
    window.addEventListener('online', this.alertOnlineStatus)
    window.addEventListener('offline', this.alertOnlineStatus)
  },
  beforeDestroy () {
    this.clearEvent()
  }
}
</script>

这里如果不需要咱们来控制什么时候用户可以更新,可以不调用接口,直接调用doUpdate方法,实现服务器上如果有文件可以更新,用户就可以更新,而不经过我方同意(不太建议)。个人觉得还是多一层控制好一些。

最后我们还需要在打包的时候配置一下应用更新的地址,这里我使用的打包工具是electron-builder,vue用的vue2。 配置如下

// vue.config.js
builderOptions: {
    publish: [
      {
        provider: 'generic',
        url: process.env.VUE_APP_UPDATEURL
      }
    ]
}

这样 我们就创建好了一整套electron的更新逻辑。可以在渲染进程中触发,在主进程中进行应用更新逻辑处理。