前言
最近帮公司搞桌面应用,从NW.js和Electron中做选择,最后选择了Electron,没啥特别难的点,vue脚手架+vue-cli-plugin-electron-builder一把梭,记录一下在线更新踩的一些坑,顺便给自己做做总结,有未完善的地方见谅。
简单介绍
Electron是什么?
一款开发跨平台桌面应用的框架。
Windows | Mac | Linux
为何使用它,有何优缺点?
优点
- 快速打包vue/react应用
- 跨平台
缺点
- 体积大,占用内存多
在线更新的技术前提
本文讨论的方案,技术前提如下:
vue/cli脚手架 + vue-cli-plugin-electron-builder插件构建的Electron应用
(使用Electron框架,直接导入vue项目的,或使用vue-electron的,原理类似,不在本文讨论范围)
在线更新方案
使用electron-updater插件进行升级
安装插件
npm install electron-updater --save
配置publish
vue.config.js文件,builder配置内添加publish,如下:
配置publish项,是为了打包后生成latest.yml文件,该文件是用于判断版本升级的。是打包过程中生成的,为避免自动升级报错,生成后禁止修改文件内容,若文件有误,需要重新打包生成。
注意,这里有个坑,如果服务器更新地址经常变动,这里的url建议不填写,在主进程获取到url后,通过api重新设置。
latest.yml文件记录了版本号、更新包的路径、包大小、日期等。
主进程代码
electron-updater插件的代码必须在主进程执行
import { autoUpdater } from 'electron-updater'
// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写
function updateHandle (updateConfig) {
let message = {
error: 'update error',
checking: 'updating...',
updateAva: 'fetch new version and downloading...',
updateNotAva: 'do not to update'
}
// 设置服务器更新地址
autoUpdater.setFeedURL({
provider: 'generic',
url: updateConfig.download
})
autoUpdater.on('error', function () {
sendUpdateMessage(message.error)
})
autoUpdater.on('checking-for-update', function () {
sendUpdateMessage(message.checking)
})
// 版本检测结束,准备更新
autoUpdater.on('update-available', function (info) {
sendUpdateMessage(message.updateAva)
})
autoUpdater.on('update-not-available', function (info) {
sendUpdateMessage(message.updateNotAva)
})
// 更新下载进度事件
autoUpdater.on('download-progress', function (progressObj) {
console.log('下载进度百分比>>>', progressObj.percent)
})
// 下载完成
autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
// 退出且重新安装
autoUpdater.quitAndInstall()
})
ipcMain.on('checkForUpdate', () => {
// 执行自动更新检查
autoUpdater.checkForUpdates()
})
// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage (text) {
mainWindow.webContents.send('message', text)
}
}
export default updateHandle
渲染进程代码
// ####### 请保证updateHandle方法在主进程已经调用过一遍,事件监听都存在
// 检测更新
ipcRenderer.send('checkForUpdate')
读取进度条组件
主进程向渲染进程页面发送进度数据,展示当前更新进度
<template>
<div class="update">
<div class="con">
<el-progress :percentage="percentage"></el-progress>
<span>正在检测版本,请稍后</span>
</div>
</div>
</template>
<script>
const { ipcRenderer } = require('electron')
export default {
name: 'update-loading',
data () {
return {
percentage: 0
}
},
created () {
let vm = this
ipcRenderer.on('download-progress', (e, data) => {
vm.percentage = Number(data)
})
}
}
在线更新实践中的坑
正确引用autoUpdater
// 不要引用electron自带的autoUpdater
// const { autoUpdater } = require('electron')
import { autoUpdater } from 'electron-updater'
服务器latest-mac.yml文件找不到
解决方案:
服务器增加yml文件格式MIME类型映射
取错本地版本号
electron-updater自身的bug,会去取Electron的版本;
解决方案:
修改electron-updater中appUpdater.js中isUpdateAvailable函数代码
const pkg=require('../../../package.json');
// const isLatestVersionNewer = (0, _semver().gt)(latestVersion, currentVersion);
const isLatestVersionNewer = (0, _semver().gt)(latestVersion, pkg.version);
开发环境提示dev-app-update.yml文件不存在
解决方案:
在updateHandle方法内,加入下面代码,地址是本地打包后的app-update.yml文件路径
if (process.env.NODE_ENV === 'development' && !isMac) {
autoUpdater.updateConfigPath = path.join(__dirname, 'win-unpacked/resources/app-update.yml')
// mac的地址是'Contents/Resources/app-update.yml'
}
下载包缓存导致的更新失败
解决方案:
updaterCacheDirName的值与src/main/app-update.yml中的updaterCacheDirName值一致,在windows中会创建一个类似 //C:\Users\Administrator\AppData\Local\electron-updater1\pending文件存储更新下载后的文件"*.exe"和"update-info.json"
每次更新前,删除本地安装包
在updateHandle方法内,加入下面代码
// 更新前,删除本地安装包
let updaterCacheDirName = 'electron-admin-updater'
const updatePendingPath = path.join(autoUpdater.app.baseCachePath, updaterCacheDirName, 'pending')
fs.emptyDir(updatePendingPath)
Mac系统更新需要代码签名
这里不做深入,有兴趣可以去了解
https://segmentfault.com/a/1190000012902525
更新应用的完整代码
import { ipcMain } from 'electron'
import { autoUpdater } from 'electron-updater'
// win是所有窗口的引用
import { createWindow, win } from '../background'
const path = require('path') // 引入path模块
const _Store = require('electron-store')
const fs = require('fs-extra')
const isMac = process.platform === 'darwin'
// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写
function updateHandle (updateConfig = undefined) {
// electron缓存
let localStore = new _Store()
// 更新配置
updateConfig = updateConfig !== undefined ? updateConfig : localStore.get('updateConfig')
// 更新前,删除本地安装包 ↓
let updaterCacheDirName = 'electron-admin-updater'
const updatePendingPath = path.join(autoUpdater.app.baseCachePath, updaterCacheDirName, 'pending')
fs.emptyDir(updatePendingPath)
// 更新前,删除本地安装包 ↑
let message = {
error: 'update error',
checking: 'updating...',
updateAva: 'fetch new version and downloading...',
updateNotAva: 'do not to update'
}
// 本地开发环境,改变app-update.yml地址
if (process.env.NODE_ENV === 'development' && !isMac) {
autoUpdater.updateConfigPath = path.join(__dirname, 'win-unpacked/resources/app-update.yml')
}
// 设置服务器更新地址
autoUpdater.setFeedURL({
provider: 'generic',
url: updateConfig.download
})
autoUpdater.on('error', function () {
sendUpdateMessage(message.error)
})
autoUpdater.on('checking-for-update', function () {
sendUpdateMessage(message.checking)
})
// 准备更新,打开进度条读取页面,关闭其他页面
autoUpdater.on('update-available', function (info) {
sendUpdateMessage(message.updateAva)
createWindow('update-loading', {
width: 500,
height: 300,
minWidth: 720,
resizable: false,
fullscreenable: false,
frame: false
})
for (let key in win) {
if (key !== 'update-loading') {
win[key] && win[key].close()
}
}
})
autoUpdater.on('update-not-available', function (info) {
sendUpdateMessage(message.updateNotAva)
})
// 更新下载进度
autoUpdater.on('download-progress', function (progressObj) {
win['update-loading'] && win['update-loading'].webContents.send('download-progress', parseInt(progressObj.percent))
})
// 更新完成,重启应用
autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
ipcMain.on('isUpdateNow', (e, arg) => {
// some code here to handle event
autoUpdater.quitAndInstall()
})
win['update-loading'] && win['update-loading'].webContents.send('isUpdateNow')
})
ipcMain.on('checkForUpdate', () => {
// 执行自动更新检查
autoUpdater.checkForUpdates()
})
// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage (text) {
win['update-loading'] && win['update-loading'].webContents.send('message', message.error)
}
}
export default updateHandle