做者:钟离,酷家乐PC客户端负责人css
原文地址:webfe.kujiale.com/electron-au…html
酷家乐客户端:下载地址 www.kujiale.com/activity/13…前端
文章背景:在酷家乐客户端在V12改版成功后,咱们积累了许多的宝贵的经验和最佳实践。前端社区里关于Electron知识相对较少,所以但愿将这些内容以系列文章的形式分享出来。ios
系列文章:git
在讲客户端更新方案以前,咱们先了解一下web和客户端更新的原理github
在web应用的世界里,咱们一般会更新web服务器上的前端代码(模板、HTML,也多是js、css),来发布新的功能。在此以后用户再访问咱们的web服务器,拿到的已是更新事后的前端代码了。web
web应用更新如此方便,得益于它中心化存储的方式:shell
浏览器缓存也算是在用户本机存储了前端代码,可是在web应用须要更新的时候,确定是会禁用缓存的,不然此次发布对有缓存的用户无效。npm
和web应用的中心化储存不一样,客户端的代码其实是一种分布式存储,每一个用户电脑上都有一份完整的代码文件,有点像git
。json
用户在电脑上安装客户端,实际上会将客户端代码文件持久储存到本机。例如在MacOS上,代码文件存放在/Applications
目录下。
客户端内嵌web页面的更新方式,和上面讲到的web应用更新是同样的,再也不赘述(参考移动APP内嵌的H5页面更新)
web应用的更新,其实是更新服务端代码文件
客户端的更新,其实是更新用户电脑上代码文件
Electron官网有关于更新的教程 Updating Applications,可是都不能知足业务需求:
update.electron.org
,代码必须托管在github上,passelectron-builder
,windows下只支持NSIS,并且须要搭建HTTP服务。更新程序UI和交互定制也不是很友好Deploying an Update Server
,这个方案须要部署一个update server,也比较麻烦所以,咱们使用的是本身实现的一套更新流程。
检查更新是总体流程的第一个步骤。若是有更新,后续的更新逻辑才会执行。一般咱们会在软件启动时检查更新。
检查更新的策略,其实是将本地客户端的版本与远程版本进行一次对比,而后根据版本对比的结果来给出不一样的更新展现。
相比于本身搭建一个update server,维护一个远程的JSON数据成本是很低的。这个远程数据能够是一个后端接口或者cdn上的json文件,而且能够在须要更新的时候,及时更新远程数据的内容
这个远程JSON数据里面通常会存放版本号、更新内容介绍以及发布时间:
const updateData = axios.get('https://some-update.json');
console.log(updateData);
/* { version: '1.0.0', changeLogs: ['来个开发祭天','新增了🐂🍺的功能'], 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
检查到软件更新以后,须要给出更新提示来提醒到用户。此时,咱们会使用一个窗口承载更新提示的内容,后面统称为更新窗口。
更新窗口内部代码示例:
const updateData = axios.get('some-update.json')
// 检查更新的逻辑,省略
if (!shouldUpdate) return; // 无更新
// 执行到这里,确定有更新了。拿到更新数据,渲染窗口内容
ReactDom.render(<App updateData={updateData} onUpdate={() => console.log('用户点击了更新')} />, '#app'); // 有更新,主动展现窗口(更新窗口默认是隐藏的) currentWindow.show(); 复制代码
当用户点击了更新按钮以后,那么意味着咱们能够开始进行最后一步了。
最后的这一步骤,咱们分两步进行:
这一步骤,能够交给用户来作,也能够由咱们帮用户来作。咱们来看看这两种状况下,分别是如何实现的。
首先,咱们须要更新网站客户端下载页上的安装程序资源至最新。而后,用户点击更新按钮以后,直接用本机默认浏览器打开下载页,让用户本身下载、安装,安装程序正常执行完毕以后,自己就能够覆盖本机代码文件。
此法用户体验不是很好,可是优势也很明显:节省了不少开发成本,直接复用了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