本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
一、前言
对于一个应用来说,版本更新是最基础的功能之一,不然咱也不能让用户每次想用最新版本都去下载,对吧。关于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的更新逻辑。可以在渲染进程中触发,在主进程中进行应用更新逻辑处理。