前言
关于 electron 更新,踩了不少坑,在此做一总结。项目使用的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 v14.17.5,electron 版本 12.0.0。本文章的方法亲测有用,读者可以按照此方法实现自动更新,如有意见和建议,欢迎评论区交流!如果对您有帮助,麻烦点赞收藏评论一波!
※注:本文代码区域每行开头的“+”表示新增,“-”表示删除,“M”表示修改;代码中的“...”表示省略。
1 全量更新
1.1 自动下载
1.1.1 方案选择
更新electron的 方案有很多种。如 electron 官方文档中推荐的 内置的 Squirrel 框架及主进程中的 autoUpdater 模块,参考:www.electronjs.org/zh/docs/lat…
我在项目的全量更新中采用的方案是 electron-builder + electron-updater + nginx 实现
要实现自动更新,应用程序的安装包应该存放在互联网的某台服务器上,每次打开应用的时候,进行自动检测,根据当前应用程序的version
和线上版本进行匹配,当发现有新的version
的时候,就自动下载,下载完成后,询问用户是否安装新版本。
1.1.2 打包不同版本安装包
修改 package.json 中的 version 字段以生成 新的版本,注意升级后的版本号要大于之前的版本,如 初始版本为 1.0.0,升级后的版本为 1.0.1,以此类推。即便没有改变程序代码,只要version 改变,也会被识别成两个版本。打包完输出目录中会生成 yml、exe、exe.blockmap 等格式的文件。
1.1.3 搭建资源服务器
我这里是在 windows 上搭建的 nginx 服务器,具体搭建方法参考此文章:juejin.cn/post/710378…
搭建好后将打包出来的 latest.yml、test1.0.1.exe、test1.0.1.exe.blockmap 文件放到指定目录下即可。我这里的访问路径为:http://127.0.0.1:9005/lotus_client/
1.1.4 代码实现
第一步:vue.config.js 中的配置:
...
module.exports = {
...
pluginOptions: {
electronBuilder: {
builderOptions: {
win: {
publish: [
{
provider: 'generic',
url: 'http://127.0.0.1:9005/', // 自动更新对应的资源路径
},
],
}
}
}
}
}
这里的 builderOptions 等同于package.json 中的 build 字段。
第二步:在应用程序主进程中调用electron-updater
模块检测更新:electron-updater 文档:www.electron.build/auto-update
checkUpdate.js:
const { autoUpdater } = require('electron-updater')
const { saveLog } = require('./main_tools.js')
const { dialog, BrowserWindow } = require('electron')
function checkUpdate() {
saveLog(`当前平台:'${process.platform}\n`)
// 苹果升级未做
if (process.platform == 'darwin') {
} else {
//我们使用 nginx 将静态资源放在 lotis_client 文件夹
//所以访问 http://127.0.0.1:9005/lotus_client 就相当于访问了lotus_client 文件夹
autoUpdater.setFeedURL('http://127.0.0.1:9005/lotus_client')
}
// autoUpdater.autoDownload = false // 将自动下载包设置为false,产品的需求是让用户自己点击更新下载
//检测更新
autoUpdater.checkForUpdates()
//监听'error'事件
autoUpdater.on('error', (err) => {
console.log(err)
saveLog(`检查更新出错:'${err}\n`)
})
autoUpdater.on('checking-for-update', function () {
saveLog(`正在检查更新…\n`)
})
//监听'update-available'事件,发现有新版本时触发
autoUpdater.on('update-available', (info) => {
saveLog(`检测到新版本${info.version}\n`)
})
// 监听下载进度
autoUpdater.on('download-progress', (info) => {
saveLog(`download-progress事件触发\n`)
saveLog(
`progress:${info.progress}\nbytesPerSecond:${info.bytesPerSecond}\npercent:${info.percent}\ntotal:${info.total}\ntransferred:${info.transferred}\n`
)
})
autoUpdater.on('update-not-available', function (info) {
saveLog(`现在使用的已经是最新版本${info.version}\n`)
})
//默认会自动下载新版本,如果不想自动下载,设置 autoUpdater.autoDownload = false
//监听'update-downloaded'事件,新版本下载完成时触发
autoUpdater.on('update-downloaded', (info) => {
saveLog(`新版本已下载完成, 版本号: ${info.version}\n`)
dialog
.showMessageBox({
type: 'info',
title: '应用更新',
message: '新版本准备就绪,即将开始安装,请勿关闭电脑!',
buttons: ['否', '是'],
})
.then((res) => {
if (res.response == 1) {
//选择是,则退出程序,安装新版本。第一个参数表示是否静默安装,第二个参数表示是否安装完启动程序
autoUpdater.quitAndInstall(true, true)
}
})
})
}
module.exports = checkUpdate
background.js:
const checkUpdate = require('./electron/checkUpdate.js')
...
const win = new BrowserWindow({...})
// 主页面一旦加载完成后就开始执行检查更新
win.webContents.on('did-finish-load', () => {
checkUpdate()
})
...
1.1.5 代码签名
做完以上步骤,打开未升级的程序会发生什么也没发生。是哪里出了问题呢?检查错误日志发现:
检查更新出错:'Error: New version 1.0.1 is not signed by the application owner: publisherNames: electron, raw info: {
"SignerCertificate": null,
"TimeStamperCertificate": null,
"Status": 2,
"StatusMessage": "未对文件 C:\\Users\\DELL\\AppData\\Local\\lotus-updater\\pending\\temp-lotus-package-V1.0.1-20220815T190219.exe 进行数字签名。无法在当前系统上运行该脚本。有关运行脚本和设置执行策略的详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies"
代码签名是什么鬼?查阅 electron-builder 文档,electron-updater参考此部分:www.electron.build/auto-update… signature validation not only on macOS, but also on Windows,也就是说 使用 electron-updater 更新要做代码签名。
为什么需要代码签名请参考:www.electronjs.org/zh/docs/lat…
做代码签名需要创建数字证书PFX,创建方法参考:juejin.cn/post/695428…
将生成的 test.pfx 文件放到项目根目录下的 public 文件夹下,然后在 vue.config.js 做如下配置:
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
win: {
// 是否在安装前对可用的更新进行签名验证。发布者名称将用于签名验证。
verifyUpdateCodeSignature: false,
// 使用的签名算法数组。对于AppX, sha256总是被使用。
signingHashAlgorithms: ['sha256'],
signDlls: false, // 是否对DLL文件签名
rfc3161TimeStampServer: 'http://timestamp.comodoca.com/rfc3161',
certificateFile: './public/test.pfx', // 代码签名证书路径
certificatePassword: 'xxx', // 代码签名证书密码,密码就是导出数字证书PFX时设的密码
}
}
}
}
electron-builder 中的详细参数可以参考:www.electron.build/configurati…
1.1.6 开始更新
打开未更新的 1.0.0 版本程序,当有如下弹窗弹出时表示整包更新的资源已经下载成功了。点击是则退出程序,安装新版本。如果点击否当前虽然不会安装,但是退出程序后会自动安装,下次再次打开程序就是最新的版本了。
checkUpdate.js 文件中记录的日志如下:
当前平台:'win32
检测到新版本,正在下载…
download-progress事件触发
progress:undefined
bytesPerSecond:115375367
percent:100
total:96223056
transferred:96223056
新版本已下载完成
1.1.7 更新原理
electron-updater 会根据上面 checkUpdate.js 中的 setFeedURL
方法指定路径下的latest.yml
中的version
来判断是否需要更新,大于当前版本的version
则需要更新,否则不更新。.yml
也是一种配置文件,有点类似于我们常用的.json
配置文件,只是两者写法不一样。
version: 1.0.1
files:
- url: lotus-package-V1.0.1-20220816T174641.exe
sha512: A3yKXk8qCCltJU2Q/hkkUKZAVywAaL94MXbThPYoqvZ++LrfZM8784tTm9+s406ozsLxUiwpOsqiQZE3NAl8mQ==
size: 96222840
path: lotus-package-V1.0.1-20220816T174641.exe
sha512: A3yKXk8qCCltJU2Q/hkkUKZAVywAaL94MXbThPYoqvZ++LrfZM8784tTm9+s406ozsLxUiwpOsqiQZE3NAl8mQ==
releaseDate: '2022-08-16T09:48:04.186Z'
1.1.8 开发调试技巧
在开发时我们可能会做开发调试,比如看下载进度呀什么的,先打了一个高版本的放在远程地址,本地重复安装低版本的看更新效果,但是在第一次更新完成后,后面的更新都是瞬间完成了,看不到进度,这里是由于electron-updater
在更新时会检测本地是否下载过这个高版本,有的话直接用本地的进行安装,我们可以把这个缓存文件删除掉。AppData这个文件夹呢可能是处于隐藏的,后面挺多地方会用到这个的,可以在顶端查看中勾选隐藏的项目让其显示
具体缓存路径如下(项目名对应package.json中的name):
win:C:\Users\Administrator(你的用户)\AppData\Local\项目名-updater
mac:~/Library/Application Support/Caches/项目名-updater
1.2 手动下载
1.2.1 流程图
1.2.2 主进程
checkUpdate.js中:
const { autoUpdater } = require('electron-updater')
const { saveLog } = require('./main_tools.js') // 写入日志的方法
const { ipcMain } = require('electron')
// webContents: 当前渲染进程窗口的 webContents
// URL: 渲染进程传递过来的更新资源指向的 URL
function checkUpdate(webContents, URL) {
// 苹果升级未做
if (process.platform == 'darwin') {
} else {
//我们使用 nginx 将静态资源放在 lotis_client 文件夹
//所以访问 http://127.0.0.1:9005/lotus_client 就相当于访问了lotus_client 文件夹
autoUpdater.setFeedURL(URL)
}
// 将自动下载包设置为false,产品的需求是让用户自己点击更新下载
//检测更新
autoUpdater.autoDownload = false
autoUpdater.checkForUpdates()
// 开始下载更新
ipcMain.on('downloadUpdate', (e) => {
autoUpdater.downloadUpdate()
})
ipcMain.on('quitAndInstall', (e) => {
//选择是,则退出程序,安装新版本
autoUpdater.quitAndInstall(true, true)
})
//监听'error'事件
autoUpdater.on('error', (err) => {
saveLog(`检查更新出错:'${err}\n`)
})
// 检测到更新时触发
autoUpdater.on('checking-for-update', function () {
saveLog(`正在检查更新…\n`)
})
//监听'update-available'事件,发现有新版本时触发
autoUpdater.on('update-available', (data) => {
webContents.send('updateEvent', {
type: 'update-available',
data,
})
})
// 监听下载进度
autoUpdater.on('download-progress', (data) => {
webContents.send('updateEvent', {
type: 'download-progress',
data,
})
})
// 这个事件触发表示已经是最新版本
autoUpdater.on('update-not-available', function (data) {
webContents.send('updateEvent', {
type: 'update-not-available',
data,
})
})
//默认会自动下载新版本,如果不想自动下载,设置 autoUpdater.autoDownload = false
//监听'update-downloaded'事件,新版本下载完成时触发
autoUpdater.on('update-downloaded', (data) => {
webContents.send('updateEvent', {
type: 'update-downloaded',
data,
})
})
}
module.exports = checkUpdate
background.js 中:
...
const win = new BrowserWindow({...})
...
// 主进程事件,登录页加载完成后触发
ipcMain.on('checkUpdate', (e, data) => {
// 生产环境才检查更新
if (process.env.NODE_ENV !== 'development') {
checkUpdate(win.webContents, data.url)
}
})
...
1.2.3 渲染进程
login.vue 登录页:
<template>
<el-dialog
title=""
:visible.sync="dialogVisible"
width="520px"
custom-class="update_dialog"
:show-close="false"
:append-to-body="true"
:close-on-press-escape="false"
:close-on-click-modal="false"
>
<div class="icon"></div>
<div class="tips" style="padding: 0 20px 0 20px">{{ tipText }}</div>
<el-progress
v-if="updateType === 'download-progress'"
:text-inside="true"
:stroke-width="20"
:percentage="percent"
></el-progress>
<div class="line" v-else></div>
<div slot="footer" class="dialog-footer">
<slot name="buttons">
<template v-if="updateType === 'update-available'">
<el-button class="button confirm_btn_confirm" @click="handleStartUpdate">是</el-button>
<el-button class="button confirm_btn_concel" type="primary" @click="handleCancelUpdate"
>否</el-button
>
</template>
<el-button
v-if="updateType === 'download-progress'"
class="button confirm_btn_confirm"
@click="handleAbortUpdate"
>取消</el-button
>
<el-button
v-if="updateType === 'update-downloaded'"
class="button confirm_btn_confirm"
@click="handleConfirmUpdate"
>确认</el-button
>
</slot>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
dialogVisible: false,
version: '',
updateType: '',
percent: 0,
},
computed: {
tipText() {
if (this.updateType === 'update-available') {
return `发现新版本,是否马上更新?`
} else if (this.updateType === 'download-progress') {
return '正在更新...'
} else if (this.updateType === 'update-downloaded') {
return '程序即将更新完成,请在确认后等待程序自动重新启动。'
}
},
},
methods: {
handleStartUpdate() {
ipcRenderer.send('downloadUpdate')
},
handleCancelUpdate() {
this.dialogVisible = false
this.updateType = ''
},
handleAbortUpdate() {
this.dialogVisible = false
},
handleConfirmUpdate() {
this.dialogVisible = false
ipcRenderer.send('quitAndInstall')
},
},
created() {
ipcRenderer.on('updateEvent', (e, data) => {
// 发现新版本
if (data.type === 'update-available') {
this.dialogVisible = true
this.updateType = data.type
this.version = data.data.version
}
// 下载中
if (data.type === 'download-progress') {
this.updateType = data.type
this.percent = parseFloat(data.data.percent.toFixed(0))
}
// 下载完成
if (data.type === 'update-downloaded') {
this.updateType = data.type
}
})
}
}
</script>
<style lang="scss">
样式略。。。
</style>
2 增量更新
待更新。。。