【Electron】酷家乐客户端开发实践分享 — 软件自动更新
更新原理
在讲客户端更新方案之前,我们先了解一下web和客户端更新的原理
web应用
在web应用的世界里,我们通常会更新web服务器上的前端代码(模板、HTML,也可能是js、css),来发布新的功能。在此之后用户再访问我们的web服务器,拿到的已经是更新过后的前端代码了。
web应用更新如此方便,得益于它中心化存储的方式:
- web应用的前端代码,一般集中储存在服务器或云服务上
- 浏览器每次都会都会去服务器拉取最新的资源, 用户本机实际上没有持久化储存web应用的代码
浏览器缓存也算是在用户本机存储了前端代码,但是在web应用需要更新的时候,肯定是会禁用缓存的,否则这次发布对有缓存的用户无效。
客户端
和web应用的中心化储存不同,客户端的代码实际上是一种 分布式存储 ,每个用户电脑上都有一份完整的代码文件,有点像 git
。
用户在电脑上安装客户端,实际上会将客户端代码文件持久储存到本机。例如在MacOS上,代码文件存放在 /Applications
目录下。
客户端内嵌web页面的更新方式,和上面讲到的web应用更新是一样的,不再赘述(参考移动APP内嵌的H5页面更新)
结论
web应用的更新,实际上是更新服务端代码文件
客户端的更新,实际上是更新用户电脑上代码文件
具体实现
Electron官网有关于更新的教程 Updating Applications ,但是都不能满足业务需求:
update.electron.org electron-builder Deploying an Update Server
因此,我们使用的是自己实现的一套更新流程。
1、检查更新
检查更新是整体流程的第一个步骤。如果有更新,后续的更新逻辑才会执行。通常我们会在 软件启动时 检查更新。
检查更新的策略,实际上是 将本地客户端的版本与远程版本 进行一次对比,然后根据版本对比的结果来给出不同的更新展示。
远程版本
相比于自己搭建一个update server,维护一个远程的JSON数据成本是很低的。这个远程数据可以是一个后端接口或者cdn上的json文件, 并且可以在需要更新的时候,及时更新远程数据的内容
这个远程JSON数据里面一般会存放版本号、更新内容介绍以及发布时间:
const updateData = axios.get('https://some-update.json'); console.log(updateData); /* { version: '1.0.0', changeLogs: ['来个开发祭天','新增了:ox::beer:的功能'], time: '2019-06-06', } */
本地版本
在Electron中获取本地版本是非常简单的 app.getVersion
const localVersion = app.getVersion(); // 0.0.1
版本对比
通常, 远程版本号大于本地版本时 ,即认定为有更新。在有更新的情况下,我们还可以根据版本号里的major、minor、patch版本变动,来制定不同的更新策略。
// 远程版本 > 本地版本 const shouldUpdate = semver.gt(removeVersion, localVersion); // 例子:major版本号变化时,给出强的更新提示。否则给出正常更新提示 const isMajorUpdate = semver.diff(removeVersion, localVersion) === 'major'; if (!shouldUpdate) return; // 无更新,不走后续 if (isMajorUpdate) { console.log('给出强势更新') } else { console.log('给出普通的更新提示') }
对于版本号的操作使用 semver
2、更新提示
检查到软件更新之后,需要给出更新提示来提醒到用户。此时,我们会使用一个窗口承载更新提示的内容,后面统称为更新窗口。
更新窗口内部代码示例:
const updateData = axios.get('some-update.json') // 检查更新的逻辑,省略 if (!shouldUpdate) return; // 无更新 // 执行到这里,肯定有更新了。拿到更新数据,渲染窗口内容 ReactDom.render( console.log('用户点击了更新')} />, '#app'); // 有更新,主动展示窗口(更新窗口默认是隐藏的) currentWindow.show();
3、更新本机文件
当用户点击了更新按钮之后,那么意味着我们可以开始进行最后一步了。
最后的这一步骤,我们分两步进行:
- 获取到最新的安装程序(.dmg or .exe),因为最新的代码文件就在安装程序中
- 替换掉用户本机上的代码文件
这一步骤,可以交给用户来做,也可以由我们帮用户来做。我们来看看这两种情况下,分别是如何实现的。
交给用户来更新
首先,我们需要更新网站客户端下载页上的安装程序资源至最新。然后,用户点击更新按钮之后, 直接用本机默认浏览器打开下载页,让用户自己下载、安装 ,安装程序正常执行完毕之后,本身就可以覆盖本机代码文件。
- 用户通过浏览器,在下载页获取到了最新的安装程序。
- 用户手动打开了安装程序,并执行完毕安装程序。
此法用户体验不是很好,但是优点也很明显:节省了很多开发成本,直接复用了web页面来做更新。
如果采用这种策略,那么代码会非常简单:
// 点击更新按钮 function handleUpdate() { shell.openExternal('https://www.kujiale.com/activity/136'); // 打开一个下载页,剩下的交给用户 }
我们帮用户更新
当然,为了追求更好的用户体验,直接在更新窗口的代码中实现功能是更好的。
第一步,下载最新的安装程序,并且给出下载进度展示。下载进度功能推荐使用 request-progress 来做。当然,也可以使用NodeJs原生的 http
和 stream
模块来实现下载进度展示,这里不详细讲解。
下载到的安装程序,可以暂时存放到用户电脑的临时文件夹中
const fs = require('fs'); const request = require('request'); const progress = require('request-progress'); // 点击更新按钮 function handleUpdate(){ // 根据版本号拼接安装程序地址 const downloadUrl = `https://someupdate/${updateData.version}/installer.dmg`; // 用request下载 progress(request(downloadUrl)) .on('progress', (state) => { // 进度 console.log(state) }) // 写入到临时文件夹 .pipe(fs.createWriteStream(path.join(app.getPath('temp'), 'installer.dmg'))) }
进度展示示例图:
第二步,将安装程序中的代码文件更新到用户本机上,此时有两种方案:
- 直接打开安装程序,用户跟随安装程序指引稍作点击即可完成安装。
- 解压安装程序中的内容,并将内容更新到用户本机
在windows下,安装程序里面是有业务逻辑的:操作注册表、卸载程序、快捷方式等等,因此我们选择第一种方案。
const { shell, app } = require('electron'); shell.openItem('your installer exe path'); // 打开下载好的安装程序 app.quit(); // 退出当前客户端
在MacOS下,我们所发行的dmg文件其实没有业务逻辑,因此可以使用方案二,直接把 .app
目录解压出来,然后拷贝到 /Applications
目录即可。在MacOS下,解压dmg文件可以使用 hdiutil 。
const cp = require('child_progress'); const path = require('path'); const fs = require('fs-extra'); // 下载完毕之后的dmg文件,文件内的.app目录名为Test const installerPath = '/your_installer.dmg'; // 使用hdiutil来解压dmg文件内部资源,解压后的资源目录为/Volumes/your_installer cproc.execSync(`hdiutil attach ${installerPath} -nobrowse`, { stdio: ['ignore', 'ignore', 'ignore'] }); // 删掉原有的.app目录 fs.removeSync('/Applications/Test.app'); // 把Volumes目录下的.app目录拷贝到/Applications中,更新完毕 fs.copySync('/Volums/your_installer/Test.app', '/Applications'); // 重启应用 app.relaunch(); app.quit();
最后
欢迎大家在评论区讨论,技术交流 & 内推 -> zhongli@qunhemail.com