新手也能看懂的 Electron 自动更新

1,828 阅读7分钟

前言

关于 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的时候,就自动下载,下载完成后,询问用户是否安装新版本。

image-20220818160359112.png

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/

image-20220816160058162.png

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 版本程序,当有如下弹窗弹出时表示整包更新的资源已经下载成功了。点击是则退出程序,安装新版本。如果点击否当前虽然不会安装,但是退出程序后会自动安装,下次再次打开程序就是最新的版本了。

image-20220816162506513.png

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 流程图

image-20220819144245861.png

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 增量更新

待更新。。。