electron的更新一般来说有两种方式,全量和增量,顾名思义全量就是下载我们打包好的exe文件或者zip文件,进行全面替换。我们之前说过electron就是用浏览器打开我们的页面,很多时候我们的更新可能只会修改渲染进程,那么我们把我们的渲染进程的文件给替换了不久更新吗,也就是说增量实际上是替换打包好的html,js等文件。那么更新的方式如下:
增量更新放到下一期来说,本篇是讲全量更新的事,本文旨在对全量更新进行详细的说明,包括主进程的更新流程和渲染进程展示等以及更新问题如何排查。
我们这里先说明一下我们的处理流程:
全量更新我们使用的是electron-updater
这个包,进入我们的项目:
npm i -D electron-updater
electron-updater更新只需要把一个url传入,然后它会自动查找${url}/xxx.yml
文件,根据这个yml
文件检测更新及下载,我们可以通过其各种事件拿到对应的更新状态,下载完成之后调用autoUpdater.quitAndInstall()
重启当前的应用并且安装更新
这个url下是放静态资源的,简单来说就是这个链接类似访问一个目录,这个目录下需要有更新的文件以及检测更新的文件。
需要哪些东西呢:
总的来说我们就是要一个url实现更新,先让渲染进程把这个url传过来吧,我们先模拟一下更新请求。
一般来说,更新都是调用后端接口,上面说了更新需要一个url,那么我们这里就用json文件模拟请求以及模拟更新文件的地址
npm i -g http-server 新建一个目录server http-server ./ -p 4000 // 启一个4000端口的服务http://127.0.0.1:4000 server下新建一个index.json来当接口返回数据 { "code": 200, "success": true, "data": { "forceUpdate": false, // 是否强制更新 "fullUpdate": true, // 是否全量更新 "upDateUrl": "http://127.0.0.1:4000/", // electron-updater传入的url "restart": true, // 增量更新是否重启 "message": "我要升级成0.0.2", // 更新说明 "version": "0.0.2" // 版本号 } } 浏览器打开http://localhost:4000/,看是否有index.json。 我们在渲染进程用请求获取index.json的数据(这里是我自己做的api请求封装) const api = proxy.$api api('http://localhost:4000/index.json', {}, { method: 'get' }).then(res => { console.log(res) })
然后我们打一个高版本包,这里是打dev包,修改.env.dev
的VUE_APP_VERSION
为0.0.2
npm run build:dev:win64 mac同理,将打包好的文件放入server目录 win:latest.yml,0.0.2的exe mac:latest-mac.yml,0.0.2的zip(注意是zip不是dmg)
不出意料的话会报一个跨域的错误,毕竟我们的端口号不同,在electron中我们的权限要比浏览器中大得多,
我们可以把同源策略关闭,就不会出现跨域的问题了,可以得到返回了。
BrowserWindow的 webPreferences: { ..... webSecurity: false }
补充:当然还有其他跨域的处理方式,想了解的话可以看一下我对跨域的说明链接
当前我们的版本号的话,是在.env
里面设置的,不知道的可看搭建篇环境变量说明。开发时不用检测版本更新,所以有个判断。
isClick是自动检测的话没有message提示,手动有。
<template> <div class="update"> <div class="version">当前版本为:{{ config.VUE_APP_VERSION }}</div> <a-button type="primary" @click="upDateClick(true)">检测更新</a-button> </div> </template> <script> import cfg from '@/config' import update from '@/utils/update' import { defineComponent, getCurrentInstance } from 'vue' export default defineComponent({ setup() { const state = reactive({ visible: false, upDateData: {}, upDateProgress: { show: false, percent: 0 } }) const { proxy } = getCurrentInstance() const config = cfg const api = proxy.$api function upDateClick(isClick) { api('http://localhost:4000/index.json', {}, { method: 'get' }).then(res => { console.log(res) if (cfg.NODE_ENV !== 'development') { update(config.VUE_APP_VERSION, res).then(() => { state.upDateData = res if (res.fullUpdate) { updateNotice(isClick) } else { console.log() } }).catch(err => { if (err.code === 0) { isClick && message.success('已为最新版本') } }) } else { message.success('请在打包环境下更新') } }) } return { config, upDateClick } } }) </script>
这里用当前版本与接口版本进行对比,向主进程发送通知(更新message弹窗)。
/utils/update.js export default (version, data) => { return new Promise((resolve, reject) => { const isUpdate = data ? versionCompare(data.version, version) : 0 switch (isUpdate) { case 0: console.log('不更新') reject({ code: isUpdate }) break; case 1: if (data.fullUpdate) { console.log('全量更新') resolve() } else { console.log('增量更新') } break case -1: console.log('降级') reject({ code: isUpdate }) break } }) } // 1为当前版本比更新版本低,0为版本一致,-1为当前版本比更新版本高 function versionCompare(stra, strb) { const straArr = stra.split('.') const strbArr = strb.split('.') const maxLen = Math.max(straArr.length, strbArr.length) let result let sa let sb for (let i = 0; i < maxLen; i++) { sa = ~~straArr[i] sb = ~~strbArr[i] if (sa > sb) { result = 1 } else if (sa < sb) { result = -1 } else { result = 0 } if (result !== 0) { return result } } return result }
检测到更新后我们显示更新弹窗,根据forceUpdate
判断如果是强制更新的话我们把弹窗的关闭除去,只能点确定。如果不是的话,那么点击取消后,我们记录一个更新版本号的localStorage
,下次再检测更新时如果有值的话,更新弹窗将不会出现
<a-modal v-model:visible="visible" title="更新" @ok="upDateOk" @cancel="upDateCancel" :closable="!upDateData.forceUpdate" :maskClosable="false" :keyboard="!upDateData.forceUpdate" :destroyOnClose="true" > <p v-html="upDateData.message"></p> <template #footer v-if="upDateData.forceUpdate"> <div class="footer"> <a-button type="primary" @click="upDateOk">确定</a-button> </div> </template> </a-modal> 调用updateNotice(isClick),这里是按钮点击检测更新,当然你也可以程序自动检测更新,放在App.vue中主动调用updateNotice(false) function updateNotice(isClick) { if (LgetItem(UPDATE_LIST) && LgetItem(UPDATE_LIST)[state.upDateData.version] && !isClick) { // 用户手动点击检测的话出现弹窗 return } state.visible = true } function upDateOk() { state.visible = false window.ipcRenderer.invoke('win-update', {...state.upDateData}) } function upDateCancel() { if (!LgetItem(UPDATE_LIST)) { LsetItem(UPDATE_LIST, {}) } LsetItem(UPDATE_LIST, { ...LgetItem(UPDATE_LIST), [state.upDateData.version]: true }) }
我们向主进程推送了更新信息win-update
ipcMain.js接收渲染进程的更新信息 import checkUpdate from './checkUpdate' ipcMain.handle('win-update', (_, data) => { checkUpdate(data) })
上面说了electron-updater需要一个url,我们这里实现一下其更新逻辑,electron-updater
有各种更新状态,我们把这些状态推送renderer-updateMsg
给渲染进程,让其有对应的提示或者进度,autoUpdater.quitAndInstall()
被调用后会自动安装重启。
新建checkUpdate.js import { autoUpdater } from 'electron-updater' import log from '../config/log.js' import global from '../config/global' autoUpdater.logger = log /** * -1 检查更新失败 0 正在检查更新 1 检测到新版本,准备下载 2 未检测到新版本 3 下载中 4 下载完成 **/ function Message(type, data) { const sendData = { type, data } global.sharedObject.win.webContents.send('renderer-updateMsg', sendData) } // 当更新发生错误的时候触发。 autoUpdater.on('error', (err) => { log.info('更新出现错误') log.info(err.message) if (err.message.includes('sha512 checksum mismatch')) { Message(-1, 'sha512校验失败') } }) // 当开始检查更新的时候触发 autoUpdater.on('checking-for-update', () => { log.info('开始检查更新') Message(0) }) // 发现可更新数据时 autoUpdater.on('update-available', () => { log.info('有更新') Message(1) }) // 没有可更新数据时 autoUpdater.on('update-not-available', () => { log.info('没有更新') Message(2) }) // 下载监听 autoUpdater.on('download-progress', (progressObj) => { Message(3, progressObj) }) // 下载完成 autoUpdater.on('update-downloaded', () => { log.info('下载完成') Message(4) setTimeout(() => { // 重启更新提示1秒后在进行重启安装 global.willQuitApp = true autoUpdater.quitAndInstall() }, 1000) }) export default function (data) { log.info('Update', data) autoUpdater.setFeedURL(data.upDateUrl) autoUpdater.checkForUpdates().catch(err => { log.info('网络连接问题', err) Message(5, err.code) }) }
在上面的更新弹窗中新增更新进度,其实就是接收主进程的更新状态做个展示:
<a-modal v-model:visible="upDateProgress.show" title="下载中" :closable="false" :maskClosable="false" :keyboard="false" :destroyOnClose="true" :footer="null" > <a-progress :percent="upDateProgress.percent" status="active" /> </a-modal> const message = proxy.$message onMounted(() => { window.ipcRenderer.on('renderer-updateMsg', (_, data) => { switch (data.type) { case -1: message.error(data.data) break case 0: message.info('正在检查更新') break case 1: message.destroy() message.success('已检查到新版本,开始下载') state.upDateProgress.show = true break case 2: message.destroy() message.success('无新版本') break case 3: state.upDateProgress.percent = data.data.percent.toFixed(1) break case 4: state.upDateProgress.show = false message.loading('重启更新中...', 1) break case 5: message.destroy() message.warning(data.data) state.upDateProgress.show = false break default: break } }) }) onUnmounted(() => { window.ipcRenderer.removeListener('renderer-updateMsg') })
开发完成后我们打个0.0.1版本的dev包(上面我们放入server文件夹的是0.0.2的dev包),然后检测更新,更新重启后看打印的config是否是0.0.2。
通过上面的步骤,我们的一个全量更新的流程就算完成了,这里对一些常见问题进行补充。
我们的请求是一个json文件,我们可能会修改其中的参数,有时候发现返回还是没变,请将控制台的Network
的Disable cache
勾上(请求缓存常见处理方式)
在开发时我们可能会做开发调试,比如看下载进度呀什么的,先打了一个高版本的放在远程地址,本地重复安装低版本的看更新效果,但是在第一次更新完成后,后面的更新都是瞬间完成了,看不到进度,这里是由于electron-updater
在更新时会检测本地是否下载过这个高版本,有的话直接用本地的进行安装,我们可以把这个缓存文件删除掉。AppData这个文件夹呢可能是处于隐藏的,后面挺多地方会用到这个的,可以在顶端查看中勾选隐藏的项目让其显示,具体的话百度吧。
electronvuedev这个是你设置的name(本框架在vue.config.js中) win:C:\Users\Administrator(你的用户)\AppData\Local\electronvuedev-updater mac:~/Library/Application Support/Caches/electronvuedev-updater
有时候我们更新时会遇到问题,比如检测出更新,但是下载却不动,或是更新完成后并没有安装重启,这里先说一下比较常见的几个可能会引发的问题
e.preventDefault()
阻止窗口的关闭,比如我们在第二期里使用了:win.on('close', (e) => { console.log('close', global.willQuitApp) if (!global.willQuitApp) { win.webContents.send('renderer-close-tips', { isMac }) e.preventDefault() } })
针对于本项目,我们在调用重启更新时,由于我们设置了这个可能会引起窗口关闭的失败,这里我们在关闭前设置willQuitApp
,让其正常关闭:
global.willQuitApp = true autoUpdater.quitAndInstall()
其他原因排查:当然不可能只有上面几种原因的,那么我们如何进行排查呢:
electron-log
这个包把autoUpdater的error收集到本地日志中:npm i electron-log 新建log.js import log from 'electron-log' log.transports.file.level = 'silly' log.transports.console.level = false // 禁用console输出 export default log 在我们的checkUpdate.js更新中使用 import log from '../config/log.js' autoUpdater.logger = log autoUpdater.on('error', (err) => { log.info('更新出现错误') log.info(err.message) })
当更新出现问题后查看对应的log文件
win:C:\Users\Administrator(你的用户)\AppData\Roaming\<app name>\logs mac: ~/Library/Logs/<app name>
本系列更新只有利用周末和下班时间整理,比较多的内容的话更新会比较慢,希望能对你有所帮助,请多多star或点赞收藏支持一下
本文地址:链接
本文github地址:链接