有任何问题,欢迎站内私信博主交流。
正文开始
- 前言
- 更新功能所有文章汇总
- 一、更新插件选择
- 二、在main.js中引入我们的更新模块
- 三、更新模块UpdateController.js暴露的方法checkUpdate
- 四、更新模块UpdateController.js中的监听
- 4.1监听是否有新版本需要更新?
- 4.2 监听更新时的下载信息
- 4.3 监听下载完成
- 五、完整的更新逻辑
- 六、优化后的监听
- 6.1新增检查更新的监听
- 6.2 新增执行更新的监听
- 6.3 新增安装的监听
- 6.4优化第四节的几个更新
- 总结
- 附件
前言
最近好久不更文,一是公司的事确实很忙,二是把时间都用在开发工具上了,写文总是提不起兴趣。
好消息是工具总算是憋出来几个,坏消息功能怎么实现的,代码快忘差不多了。我经常看着我写过的代码一脸茫然:这真的是我写的???
软件目前功能汇总:svga预览、node版本管理、前端部署-nginx管理、webstorm破解、浏览器插件等。
围绕这些功能,可能会重新开一个文章系列——pc工具源码系列,详细讲解它们都是怎么实现的。但是对它们的讲解放在这里不太合适,因为本系列主要讲解的是electron的基础技能,demo只是辅助。
唠了这么多,其实本文重点要讲解的内容是——electron更新。
更新功能所有文章汇总
- electron-updater实现electron全量更新和增量更新——主进程部分
- electron-updater实现electron全量更新和增量更新——注意事项/技巧汇总
- electron-updater实现electron全量更新和增量更新——渲染进程UI部分
- electron-updater实现electron全量更新和增量更新——渲染进程交互部分
一、更新插件选择
官网给了简单的更新api:autoUpdater,可以自行查看。
本文采取的是更新插件:electron-updater。
选择它的理由也很简单,因为它是electron-builder打包工具推荐的更新插件,看过前面文章的同学应该知道,我的项目打包工具都是基于electron-builder。
所以选择electron-updater不仅可以和electron-builder更契合,也会有便捷的增量更新功能。简单好用,就是理由。
二、在main.js中引入我们的更新模块
能接触到更新这一步的同学,手里的项目肯定是已经存在main.js等文件。
如果把所有的逻辑都放到main.js中,最终main.js会过于臃肿。为了更好地组织代码,我们需要把更新部分的功能放到一个独立的文件中:UpdateController.js
在main.js中引入这个更新功能模块,代码可能如下:
//引入更新功能
const checkUpdate = require('./controller/UpdateController');
app.whenReady().then(() => {
let win = new getWindow().createWindow() //创建窗口
const ipcSend=require('./ipc/ipc-send')
ipcSend.init(win) //监听渲染进程
checkUpdate(win); //检查更新
new getMenuPersonal(win).createMenu() //创建工具栏
// 注册快捷键监听器
getGlobalShortcut.create(win)
})
上面和更新相关的代码主要是两行:
const checkUpdate = require('./controller/UpdateController');
checkUpdate(win); //检查更新
checkUpdate 是UpdateController中暴露的方法,接收一个window对象,这个window对象用于主进程向渲染进程主动通信时使用:mainWin.webContents.send,后文会涉及。
三、更新模块UpdateController.js暴露的方法checkUpdate
首先通过npm下载electron-updater:
npm i electron-updater
然后再更新模块中引用electron-updater
const { autoUpdater } = require('electron-updater');
上一节把更新模块放到main.js中时,我们提到过更新模块暴露的方法checkUpdate,下面我们来看一下它的具体实现:
let mainWin = null;
const checkUpdate = (win) => {
mainWin = win;
if(app.isPackaged){
autoUpdater.setFeedURL('http://xxxxx:8888/updater/')
}else{
autoUpdater.setFeedURL('http://localhost:8888/updater/')
}
autoUpdater.forceDevUpdateConfig = true //开发环境下强制更新
autoUpdater.autoDownload = false; // 自动下载
autoUpdater.autoInstallOnAppQuit = true; // 应用退出后自动安装
};
可以看到我的checkUpdate实现十分简单,它主要的作用就是操作autoUpdater对象完成一些基础配置。
- 我们在方法体外定义了一个mainWin全局变量,checkUpdate方法中,首先为mainWin变量赋值从main.js中传来的window对象。
- autoUpdater.setFeedURL是设置更新的远程地址,设置的地址下应当能直接看到我们的exe文件,electron-updater插件会自动从这个远程地址下,获取最新安装包。
- 下面是autoUpdater的一些配置,重点注意autoDownload要设置为false,不要在检测到更新时,自动下载,对用户体验不好。我们应该能让用户控制下载、跳过下载等操作。
我不喜欢把和下载相关的所有操作一股脑都放到checkUpdate方法中,尤其是一些监听。checkUpdate的职责应该单一而纯粹,只是在做一些autoUpdater对象的基础配置。
如果你不喜欢分层分类地去构建代码,那把和更新相关的监听都放到这个方法,也是可行的。
四、更新模块UpdateController.js中的监听
更新模块的职责其实可以很简单,它可以分为两个部分,就完成最基础的更新功能:
- 一是对autoUpdater对象的配置,让更新插件知道以什么效果去执行更新。这部分工作上面已经做了。
- 二是监听更新全生命周期,让electron知道更新进行到哪一步了,都需要做什么操作。
这就是最简单的一个更新功能。这节内容,就是要监听更新的全生命周期。
4.1监听是否有新版本需要更新?
autoUpdater.on('update-available', (info) => {
console.log('有新版本需要更新',info);
//这里可以写个主进程到渲染进程的通信,主动告诉渲染进程;
//因为我实际项目中的逻辑要略复杂,所以这里先省略
});
autoUpdater.on('update-not-available', (info) => {
console.log('无需更新');
//业务代码
});
4.2 监听更新时的下载信息
autoUpdater.on('download-progress', (prog) => {
let speed=prog.bytesPerSecond / 1000000>1?Math.ceil(prog.bytesPerSecond / 1000000)+'M/s':Math.ceil(prog.bytesPerSecond / 1000)+'K/s'
mainWin.webContents.send('pc-update-progress', {
speed, // 网速
percent: Math.ceil(prog.percent), // 百分比
});
});
prog参数里,有更新过程中所有的信息,我们可以根据里面信息来计算我们需要的参数。网速就是个估算值,较真你就输了,但是百分比必须要准,不能学某些软件,前面百分之99用时1秒钟,最后百分之1用时1小时。
4.3 监听下载完成
autoUpdater.on('update-downloaded', (info) => {
isDownloading=false
mainWin.webContents.send('pc-downloaded'); //告诉页面,更新完成了
// 下载完成后强制用户安装,不推荐
// autoUpdater.quitAndInstall();
});
监听的最简代码至此就完成了。
五、完整的更新逻辑
如果按照上面的代码照搬,大概率是不会触发更新的,因为里面还缺少了一个关键的触发方法:autoUpdater.checkForUpdatesAndNotify()。这个方法是检查更新的api,只有调用它,才会触发后续一系列监听。
大部分文章都把这个方法放到checkUpdate方法中,意味着当electron主进程加载时,就会调用checkUpdate方法,此时就会同步检查更新,并触发对应的监听方法。
可这样合理吗?会不会更新逻辑运行完,向渲染进程通信了消息,但是渲染进程还未结束,导致显示出现异常?可能会有人在checkUpdate方法执行的地方增加setTimeout,以确保更新的逻辑都正常运行。但是当项目变大,逻辑变复杂后,写setTimeout强行异步的方式,就是混乱之源。
我们正常的更新逻辑,不应该是主进程加载后,就检查更新,而应该是页面加载后,检查更新,并获取更新模块反馈的信息。
因为你不知道是主进程更新逻辑运行得快,还是页面渲染得快。即使在某些电脑上,主进程更新逻辑运行速度优于页面渲染速度,最终表现正常,也无法保证在不同性能电脑上都能表现一致。
所以现在的完整逻辑就是:
第一步:页面渲染完毕,并询问主进程,是否有更新?
第二步:主进程检查更新,并反馈给页面,有更新/无更新。
第三步:如果无更新,页面直接显示提示信息。如果有更新,页面产生交互逻辑,将决定权交给用户,用户决定是否更新。
第四步:用户点击更新,页面将指令发送给主进程,主进程开始执行更新。
六、优化后的监听
6.1新增检查更新的监听
经过优化后,我们需要设计有用户交互逻辑的更新功能,第一步就是要监听页面渲染完毕后,询问主进程是否有更新,并把结果反馈给页面,这是一个双向通信。
很多监听都会给页面反馈消息,所以我们创建一个反馈信息的全局变量:judgeRs。
用户可能会刷新页面,这时页面会重新渲染,重新发送检查更新的信息,如果不加控制,就会出现重复的更新下载,所以我们创建一个控制是否检查更新的全局变量:isDownloading。
let judgeRs={}
let isDownloading=false
ipcMain.handle('check-pc-update',async ()=>{
try {
if(isDownloading){
return {
success:true,
isDownloading:true,
msg:'正在下载中,请稍后'
}
}else{
const res= await autoUpdater.checkForUpdatesAndNotify()
console.log('judge',res)
//如果check结果正常,则使用上面监听构造的judgeRs
return judgeRs
}
}catch (e){
// check报错
judgeRs = {
success: false,
msg: '没有更新包:博主财力有限,服务器被下架了,软件最新版本,请通过"中二少年工具箱"小程序,查询网盘下载地址'
}
return judgeRs
}
})
6.2 新增执行更新的监听
通过上面优化后的更新逻辑,我们知道,更新操作不再是自动进行,而是由用户点击按钮操作的。所以要监听用户的操作,并触发更新。这是由渲染进程到主进程的单向通信。
/*监听渲染进程指令,执行更新*/
ipcMain.on('send-update', () => {
autoUpdater.autoDownload = true;
autoUpdater.checkForUpdates();
})
注意autoUpdater.autoDownload = true;这就是在checkUpdate方法中,为什么要把autoDownload默认设置成false,因为如果默认是true,就无法实现由用户控制更新。在’check-pc-update’监听中,执行检查更新autoUpdater.checkForUpdatesAndNotify()方法时,就会自动更新下载安装包。我们就是通过autoDownload 属性的开闭,来实现是否下载的控制。
6.3 新增安装的监听
更新下载完毕后,是否立即安装,也应该由用户控制。
// 监听渲染进程的 install 事件,触发退出应用并安装
ipcMain.handle('pc-install', () => autoUpdater.quitAndInstall());
6.4优化第四节的几个更新
在本节中,增加了两个全局变量judgeRs、isDownloading。
judgeRs在是否有更新的监听中,可以赋值,如下:
autoUpdater.on('update-available', (info) => {
console.log('有新版本需要更新',info);
judgeRs={
success:true,
needUpdate:true,
msg:'有新版本需要更新',
version:info.version
}
});
autoUpdater.on('update-not-available', (info) => {
console.log('无需更新');
judgeRs={
success:true,
needUpdate:false,
msg:'无需更新'
}
});
当监听到正在下载资源时,可以把isDownloading赋值为true:
autoUpdater.on('download-progress', (prog) => {
let speed=prog.bytesPerSecond / 1000000>1?Math.ceil(prog.bytesPerSecond / 1000000)+'M/s':Math.ceil(prog.bytesPerSecond / 1000)+'K/s'
mainWin.webContents.send('pc-update-progress', {
speed, // 网速
percent: Math.ceil(prog.percent), // 百分比
});
isDownloading=true
});
至此,主进程所有的更新操作就完成了。后续还可以增加强制更新、回退版本等各种功能。
总结
大家如果需要联系博主,或者获取博主各系列文章对应的资源,可以通过私信博主来获取。
有任何前端项目、demo、教程需求,都可以联系博主,博主会视精力更新,免费的羊毛,不薅白不薅!~
附件
更新模块UpdateController完整的代码参考:
const { autoUpdater } = require('electron-updater');
const {ipcMain,app} = require('electron')
let mainWin = null;
let judgeRs={}
let isDownloading=false
const checkUpdate = (win) => {
mainWin = win;
if(app.isPackaged){
autoUpdater.setFeedURL('http://lizetoolbox.top:83/updater/lize-tools-pc')
}else{
autoUpdater.setFeedURL('http://localhost:83/updater/lize-tools-pc/')
}
autoUpdater.forceDevUpdateConfig = true //开发环境下强制更新
autoUpdater.autoDownload = false; // 自动下载
autoUpdater.autoInstallOnAppQuit = true; // 应用退出后自动安装
};
autoUpdater.on('update-available', (info) => {
console.log('有新版本需要更新',info);
judgeRs={
success:true,
needUpdate:true,
msg:'有新版本需要更新',
version:info.version
}
});
autoUpdater.on('update-not-available', (info) => {
console.log('无需更新');
judgeRs={
success:true,
needUpdate:false,
msg:'无需更新'
}
});
// 监听渲染进程的 install 事件,触发退出应用并安装
ipcMain.handle('pc-install', () => autoUpdater.quitAndInstall());
ipcMain.handle('check-pc-update',async ()=>{
try {
if(isDownloading){
return {
success:true,
isDownloading:true,
msg:'正在下载中,请稍后'
}
}else{
const res= await autoUpdater.checkForUpdatesAndNotify()
console.log('judge',res)
//如果check结果正常,则使用上面监听构造的judgeRs
return judgeRs
}
}catch (e){
// check报错
judgeRs = {
success: false,
msg: '没有更新包:博主财力有限,服务器被下架了,软件最新版本,请通过"中二少年工具箱"小程序,查询网盘下载地址'
}
return judgeRs
}
})
autoUpdater.on('download-progress', (prog) => {
let speed=prog.bytesPerSecond / 1000000>1?Math.ceil(prog.bytesPerSecond / 1000000)+'M/s':Math.ceil(prog.bytesPerSecond / 1000)+'K/s'
mainWin.webContents.send('pc-update-progress', {
speed, // 网速
percent: Math.ceil(prog.percent), // 百分比
});
isDownloading=true
});
autoUpdater.on('update-downloaded', (info) => {
isDownloading=false
mainWin.webContents.send('pc-downloaded');
// 下载完成后强制用户安装,不推荐
// autoUpdater.quitAndInstall();
});
/*监听渲染进程指令,执行更新*/
ipcMain.on('send-update', () => {
autoUpdater.autoDownload = true;
autoUpdater.checkForUpdates();
})
module.exports = checkUpdate;