electron-updater实现electron全量更新和增量更新——主进程部分

同学们可以私信我加入学习群!


正文开始


前言

最近好久不更文,一是公司的事确实很忙,二是把时间都用在开发工具上了,写文总是提不起兴趣。

好消息是工具总算是憋出来几个,坏消息功能怎么实现的,代码快忘差不多了。我经常看着我写过的代码一脸茫然:这真的是我写的???

软件目前功能汇总:svga预览、node版本管理、前端部署-nginx管理、webstorm破解、浏览器插件等。

围绕这些功能,可能会重新开一个文章系列------pc工具源码系列,详细讲解它们都是怎么实现的。但是对它们的讲解放在这里不太合适,因为本系列主要讲解的是electron的基础技能,demo只是辅助。

唠了这么多,其实本文重点要讲解的内容是------electron更新。

更新功能所有文章汇总

  1. electron-updater实现electron全量更新和增量更新------主进程部分
  2. electron-updater实现electron全量更新和增量更新------注意事项/技巧汇总
  3. electron-updater实现electron全量更新和增量更新------渲染进程UI部分
  4. 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中引入这个更新功能模块,代码可能如下:

c 复制代码
//引入更新功能
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)
})

上面和更新相关的代码主要是两行:

c 复制代码
const checkUpdate = require('./controller/UpdateController');
checkUpdate(win);  //检查更新

checkUpdate 是UpdateController中暴露的方法,接收一个window对象,这个window对象用于主进程向渲染进程主动通信时使用:mainWin.webContents.send,后文会涉及。

三、更新模块UpdateController.js暴露的方法checkUpdate

首先通过npm下载electron-updater:

c 复制代码
npm i electron-updater

然后再更新模块中引用electron-updater

c 复制代码
const { autoUpdater } = require('electron-updater');

上一节把更新模块放到main.js中时,我们提到过更新模块暴露的方法checkUpdate,下面我们来看一下它的具体实现:

c 复制代码
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中的监听

更新模块的职责其实可以很简单,它可以分为两个部分,就完成最基础的更新功能:

  1. 一是对autoUpdater对象的配置,让更新插件知道以什么效果去执行更新。这部分工作上面已经做了。
  2. 二是监听更新全生命周期,让electron知道更新进行到哪一步了,都需要做什么操作。

这就是最简单的一个更新功能。这节内容,就是要监听更新的全生命周期。

4.1监听是否有新版本需要更新?

c 复制代码
autoUpdater.on('update-available', (info) => {
    console.log('有新版本需要更新',info);
    //这里可以写个主进程到渲染进程的通信,主动告诉渲染进程;
    //因为我实际项目中的逻辑要略复杂,所以这里先省略
});
autoUpdater.on('update-not-available', (info) => {
    console.log('无需更新');
    //业务代码
});

4.2 监听更新时的下载信息

c 复制代码
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 监听下载完成

c 复制代码
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。

c 复制代码
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 新增执行更新的监听

通过上面优化后的更新逻辑,我们知道,更新操作不再是自动进行,而是由用户点击按钮操作的。所以要监听用户的操作,并触发更新。这是由渲染进程到主进程的单向通信。

c 复制代码
/*监听渲染进程指令,执行更新*/
ipcMain.on('send-update', () => {
    autoUpdater.autoDownload = true;
    autoUpdater.checkForUpdates();
})

注意autoUpdater.autoDownload = true;这就是在checkUpdate方法中,为什么要把autoDownload默认设置成false,因为如果默认是true,就无法实现由用户控制更新。在'check-pc-update'监听中,执行检查更新autoUpdater.checkForUpdatesAndNotify()方法时,就会自动更新下载安装包。我们就是通过autoDownload 属性的开闭,来实现是否下载的控制。

6.3 新增安装的监听

更新下载完毕后,是否立即安装,也应该由用户控制。

c 复制代码
// 监听渲染进程的 install 事件,触发退出应用并安装
ipcMain.handle('pc-install', () => autoUpdater.quitAndInstall());

6.4优化第四节的几个更新

在本节中,增加了两个全局变量judgeRs、isDownloading。

judgeRs在是否有更新的监听中,可以赋值,如下:

c 复制代码
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:

c 复制代码
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完整的代码参考:

c 复制代码
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;
相关推荐
杨江1 分钟前
ThingsBoard安装测试
服务器·数据库
mit6.82410 分钟前
[Redis#4] string | 常用命令 | + mysql use:cache | session
数据库·redis·后端·缓存
明月清风徐徐11 分钟前
Vue实训---2-路由搭建
前端·javascript·vue.js
鸽鸽程序猿38 分钟前
【前端】javaScript
开发语言·前端·javascript
秦时明月之君临天下1 小时前
React和Next.js的相关内容
前端·javascript·react.js
Beekeeper&&P...1 小时前
map和redis关系
数据库·redis·缓存
jianqimingtian1 小时前
如何使用 Matlab 制作 GrabCAD 体素打印切片
数据结构·数据库
真真假假々1 小时前
MySQL和ADSDB
数据库·mysql
秦老师Q1 小时前
MySQL第二章 sql约束与sql数据类型
数据库·sql·mysql
米奇妙妙wuu1 小时前
React中 setState 是同步的还是异步的?调和阶段 setState 干了什么?
前端·javascript·react.js