当前位置: 首页 > news >正文

electron-egg实现全量更新和增量更新(上)

electron-egg 是一个 业务框架

就好比 Spring 之于 JAVA,Thinkphp 之于 PHP,Nuxt.js 之于 VUE ......

electron提供了基础的函数和api,当你写项目的时候,业务和代码工程化是需要自己实现的,electron-egg 就提供了这个工程化能力

在 Electron 应用中,更新机制是维护应用健康的重要部分。主要有两种更新方式:全量更新和增量更新。好,在最近的开发中,也需要做这样的全量更新和增量更新的功能,这里就分享一下关于全量更新和增量更新的业务代码,首先还是要介绍一下什么是全量更新和增量更新:

全量更新 是指每次更新时下载完整的应用程序包(通常是整个应用的压缩包或安装包),替换掉当前安装的版本。

特点

  • 下载量大:每次更新都需要下载完整的应用包

  • 实现简单:不需要复杂的差异计算和补丁应用逻辑

  • 可靠性高:全新安装,避免因增量更新导致的文件不一致问题

  • 适合场景:

    • 应用体积不大

    • 更新频率低

    • 网络条件良好

缺点:下载量大,electron 打包生成的桌面软件,小则50-80多MB,多则几百多MB,所以当我们的软件放在应用市场或者云空间(如:七牛云)上面供用户下载安装,如果我们平时软件需要更新动不动就走全量更新,那对流量的消耗也是比较大的,相对于成本也就上来了,所以对于一些小的版本更新我们没必要走全量更新

增量更新 是指只下载新旧版本之间的差异部分(delta/diff),然后在本地合并这些差异来生成新版本。

特点

  • 下载量小:只传输变更部分,节省带宽

  • 实现复杂:需要服务端计算差异,客户端应用补丁

  • 潜在风险:补丁应用失败可能导致应用损坏

  • 适合场景:

    • 应用体积大

    • 更新频繁

    • 用户网络条件参差不齐

缺点:开发比较复杂

接下来我们就用代码来实现 electron-egg 的全量更新和增量更新

全量更新

// 首先安装electron-egg
git clone https://gitee.com/dromara/electron-egg.git
# 进入目录 ./electron-egg/
npm install//如果下载慢:设置国内镜像源(加速)
//在根目录添加 .npmrc 文件,添加如下内容

registry=https://registry.npmmirror.com/
disturl=https://registry.npmmirror.com/-/binary/node
electron_mirror=https://npmmirror.com/mirrors/electron/
electron-builder-binaries_mirror=https://registry.npmmirror.com/-/binary/electron-builder-binaries/

//进入frontend目录安装
cd frontend
npm i
//这里演示我是使用element-plus 来做ui界面,可根据自己喜好选择
npm i element-plus
npm i
crypto-js

安装好后,进入electron/service/os目录下创建  updater.js, 添加如下代码:

const {is} = require("ee-core/utils");
const {app: electronApp} = require("electron");
const { logger } = require('ee-core/log');
const { autoUpdater } = require("electron-updater");
const { getMainWindow, setCloseAndQuit} = require("ee-core/electron");class UpdaterService {constructor() {this.config = {windows: true,macOS: true,linux: false,options: {provider: 'generic',url: is.windows() ? 'http://cos.xxxxxx.com/software/windows/xxxxxx/':'http://cos.xxxxxx.com/software/macos/xxxxxx',autoDownload: false},}this.versionType = {largeUpdate: 'largeUpdate',smallUpdate: 'smallUpdate',noAvailable: 'noAvailable'}}checkForUpdates() {const cfg = this.config;if ((is.windows() && cfg.windows) || (is.macOS() && cfg.macOS) || (is.linux() && cfg.linux)) {// continue} else {return}const status = {error: -1,available: 1,   //检测更新状态noAvailable: 2,downloading: 3,downloaded: 4}// 获取当前软件版本const currentVersion = electronApp.getVersion();// 设置下载服务器地址let server = cfg.options.url;let lastChar = server.substring(server.length - 1);server = lastChar === '/' ? server : server + "/";cfg.options.url = server;try {autoUpdater.autoDownload = cfg.options.autoDownload;autoUpdater.setFeedURL(cfg.options);} catch (error) {logger.error('[IncrementUpdater] setFeedURL error : ', error);}/*** {*   version: '5.0.2',*   files: [*     {*       url: 'ee-win-5.0.2-x64.exe',*       sha512: 'BXkvlEdSVMVm+wJNOF2zoTLj/nMbqm+00LXwuh4Jv/MEpKbQJtlZRG5h+neGN+WkqsBK/CHpMlnk2Bza4tCGzA==',*       size: 84662061*     }*   ],*   path: 'ee-win-5.0.2-x64.exe',*   sha512: 'BXkvlEdSVMVm+wJNOF2zoTLj/nMbqm+00LXwuh4Jv/MEpKbQJtlZRG5h+neGN+WkqsBK/CHpMlnk2Bza4tCGzA==',*   releaseDate: '2025-01-18T07:10:02.522Z'* }*/autoUpdater.on('update-available', (res) => {logger.info('[IncrementUpdater] update-available : ', res);//有可用更新const lastVersion = res.version;const versionResult = this.compareSemVer(currentVersion,lastVersion)const data = {status: status.available,desc: '有可用更新',versionType: versionResult}this.sendStatusToWindow(data)})autoUpdater.on('download-progress', (progressObj) => {const percentNumber = parseInt(progressObj.percent);const totalSize = this.bytesChange(progressObj.total);const transferredSize = this.bytesChange(progressObj.transferred);let text = '已下载 ' + percentNumber + '%';text = text + ' (' + transferredSize + "/" + totalSize + ')';const data = {status: status.downloading,desc: text,percentNumber,totalSize,transferredSize}// logger.info('[autoUpdater] progress: ', text);this.sendStatusToWindow(data);})autoUpdater.on('update-downloaded', () => {const data = {status: status.downloaded,desc: '下载完成'}this.sendStatusToWindow(data);// logger.info('[autoUpdater] 下载完成,正在退出应用,请稍后...');// 托盘插件里面设置了阻止窗口关闭,这里设置允许关闭窗口setCloseAndQuit(true);// Install updates and exit the application
            autoUpdater.quitAndInstall();});autoUpdater.on('update-not-available', () => {const data = {status: status.noAvailable,desc: '没有可用更新'}// this.sendStatusToWindow(data);
        })autoUpdater.on('error', (err) => {const data = {status: status.error,desc: err}this.sendStatusToWindow(data);})}/*** 下载更新*/download () {autoUpdater.downloadUpdate();}/*** 单位转换*/bytesChange (limit) {let size = "";if(limit < 0.1 * 1024){size = limit.toFixed(2) + "B";}else if(limit < 0.1 * 1024 * 1024){size = (limit/1024).toFixed(2) + "KB";}else if(limit < 0.1 * 1024 * 1024 * 1024){size = (limit/(1024 * 1024)).toFixed(2) + "MB";}else{size = (limit/(1024 * 1024 * 1024)).toFixed(2) + "GB";}let sizeStr = size + "";let index = sizeStr.indexOf(".");let dou = sizeStr.substring(index + 1 , index + 3);if(dou == "00"){return sizeStr.substring(0, index) + sizeStr.substring(index + 3, index + 5);}return size;}/*** 检查是否有新版本*/checkForUpdater() {autoUpdater.checkForUpdates()return null;}/*** 向前端发消息*/sendStatusToWindow(content = {}) {const textJson = JSON.stringify(content);const channel = 'custom/app/updater';const win = getMainWindow();win.webContents.send(channel, textJson);}/*** 获取配置* @returns {*|{linux: boolean, options: {provider: string, autoDownload: boolean, url: string}, windows: boolean, macOS: boolean}}*/getConfig(){return this.config;}/*** 比较版本,这里用了一个小技巧,就是获取版本号的时候通过主版本号判断是否是全量更新还是增量更新* @param version1 当前软件的版本号* @param version2 最新软件版本号* @returns {string|string}*/compareSemVer(version1, version2) {logger.info('[compareSemVer] version1: ', version1, ' version2: ', version2);// 将版本号字符串拆分为主版本、次版本和修订号数组const [major1, minor1, patch1] = version1.split('.').map(Number);const [major2, minor2, patch2] = version2.split('.').map(Number);// 比较主版本if (major1 !== major2 && major1 < major2) {return this.versionType.largeUpdate;}// 主版本相同,比较次版本if (minor1 !== minor2 || patch1 !== patch2) {return this.versionType.smallUpdate;}// 版本号完全相同  不需要更新return this.versionType.noAvailable}
}const updaterService = new UpdaterService();UpdaterService.toString = () => '[class UpdaterService]'module.exports = {updaterService: updaterService
}

进入electron/controller目录下创建 framework.js,添加代码:

'use strict';
const path = require('path');
const fs = require('fs');
const { exec } = require('child_process');
const { getExtraResourcesDir } = require('ee-core/ps');
const { logger } = require('ee-core/log');
const { updaterService } = require('../service/os/updater');
/*** framework - demo* @class*/
class FrameworkController {/*** 调用其它程序(exe、bash等可执行程序)**/openSoftware(args) {const { softName } = args;const softwarePath = path.join(getExtraResourcesDir(), softName);logger.info('[openSoftware] softwarePath:', softwarePath);// 检查程序是否存在if (!fs.existsSync(softwarePath)) {return false;}// 命令行字符串 并 执行, start 命令后面的路径要加双引号const cmdStr = `start "${softwarePath}"`;exec(cmdStr);// 方法二// 使用cross模块return true;}/*** 检查是否有新版本*/checkForUpdater() {updaterService.checkForUpdater();return null;}
}
FrameworkController.toString = () => '[class FrameworkController]';module.exports = FrameworkController;

修改electron/preload/index.js

/*************************************************** preload为预加载模块,该文件将会在程序启动时加载 ***************************************************/const { logger } = require('ee-core/log');
const { updaterService } = require('../service/os/updater');
function preload() {logger.info('[preload] load 1');updaterService.checkForUpdates()
}/*** 预加载模块入口*/
module.exports = {preload
}

接着我们需要创建路由文件:frontend/src/api目录下创建 electronApi.js 

const ipcApiRoute = {framework: {//检查更新checkForUpdater: 'controller/framework/checkForUpdater',//下载downloadApp: 'controller/framework/downloadApp',openSoftware: 'controller/framework/openSoftware',//下载增量更新包downloadAsar: 'controller/framework/downloadAsar',//获取本地配置getLocalStorage: 'controller/framework/getLocalStorage',//设置本地配置setLocalStorage: 'controller/framework/setLocalStorage',createNewWindow: 'controller/framework/createNewWindow',}
}/*** Customize Channel* Format: Custom (recommended to add a prefix)*/
const specialIpcRoute = {appUpdater: 'custom/app/updater', // updater channelcreateNewWindow: 'custom/app/createNewWindow'
}export {ipcApiRoute,specialIpcRoute
}

frontend/src目录下创建 config目录 index.js ,代码如下:

const DEFAULT_CONFIG = {//是否加密localStorage, 为空不加密,可填写AES(模式ECB,移位Pkcs7)加密LS_ENCRYPTION: '',
LS_ENCRYPTION_key: '',
}
export default DEFAULT_CONFIG

frontend/src/utils 目录下创建 tool.js, 代码如下:

import CryptoJS from 'crypto-js';
import sysConfig from "@/config";const tool = {}/* localStorage */
tool.data = {set(key, data, datetime = 0) {//加密if(sysConfig.LS_ENCRYPTION == "AES"){data = tool.crypto.AES.encrypt(JSON.stringify(data), sysConfig.LS_ENCRYPTION_key)}let cacheValue = {content: data,datetime: parseInt(datetime) === 0 ? 0 : new Date().getTime() + parseInt(datetime) * 1000}return localStorage.setItem(key, JSON.stringify(cacheValue))},get(key) {try {const value = JSON.parse(localStorage.getItem(key))if (value) {let nowTime = new Date().getTime()if (nowTime > value.datetime && value.datetime != 0) {localStorage.removeItem(key)return null;}//解密if(sysConfig.LS_ENCRYPTION == "AES"){value.content = JSON.parse(tool.crypto.AES.decrypt(value.content, sysConfig.LS_ENCRYPTION_key))}return value.content}return null} catch (err) {return null}},remove(key) {return localStorage.removeItem(key)},clear() {return localStorage.clear()}
}/*sessionStorage*/
tool.session = {set(table, settings) {var _set = JSON.stringify(settings)return sessionStorage.setItem(table, _set);},get(table) {var data = sessionStorage.getItem(table);try {data = JSON.parse(data)} catch (err) {return null}return data;},remove(table) {return sessionStorage.removeItem(table);},clear() {return sessionStorage.clear();}
}/*cookie*/
tool.cookie = {set(name, value, config={}) {var cfg = {expires: null,path: null,domain: null,secure: false,httpOnly: false,...config}var cookieStr = `${name}=${escape(value)}`if(cfg.expires){var exp = new Date()exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000)cookieStr += `;expires=${exp.toGMTString()}`}if(cfg.path){cookieStr += `;path=${cfg.path}`}if(cfg.domain){cookieStr += `;domain=${cfg.domain}`}console.log(document)document.cookie = cookieStr},get(name){var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"))if(arr != null){return unescape(arr[2])}else{return null}},remove(name){var exp = new Date()exp.setTime(exp.getTime() - 1)document.cookie = `${name}=;expires=${exp.toGMTString()}`}
}/* Fullscreen */
tool.screen = function (element) {var isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement);if(isFull){if(document.exitFullscreen) {document.exitFullscreen();}else if (document.msExitFullscreen) {document.msExitFullscreen();}else if (document.mozCancelFullScreen) {document.mozCancelFullScreen();}else if (document.webkitExitFullscreen) {document.webkitExitFullscreen();}}else{if(element.requestFullscreen) {element.requestFullscreen();}else if(element.msRequestFullscreen) {element.msRequestFullscreen();}else if(element.mozRequestFullScreen) {element.mozRequestFullScreen();}else if(element.webkitRequestFullscreen) {element.webkitRequestFullscreen();}}
}/* 复制对象 */
tool.objCopy = function (obj) {return JSON.parse(JSON.stringify(obj));
}/* 日期格式化 */
tool.dateFormat = function (date, fmt='yyyy-MM-dd hh:mm:ss') {date = new Date(date)var o = {"M+" : date.getMonth()+1,                 //月份"d+" : date.getDate(),                    //"h+" : date.getHours(),                   //小时"m+" : date.getMinutes(),                 //"s+" : date.getSeconds(),                 //"q+" : Math.floor((date.getMonth()+3)/3), //季度"S"  : date.getMilliseconds()             //毫秒
    };if(/(y+)/.test(fmt)) {fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));}for(var k in o) {if(new RegExp("("+ k +")").test(fmt)){fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));}}return fmt;
}/* 千分符 */
tool.groupSeparator = function (num) {num = num + '';if(!num.includes('.')){num += '.'}return num.replace(/(\d)(?=(\d{3})+\.)/g, function ($0, $1) {return $1 + ',';}).replace(/\.$/, '');
}/* 常用加解密 */
tool.crypto = {//MD5加密
    MD5(data){return CryptoJS.MD5(data).toString()},//BASE64加解密
    BASE64: {encrypt(data){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data))},decrypt(cipher){return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8)}},//AES加解密
    AES: {encrypt(data, secretKey, config={}){if(secretKey.length % 8 != 0){console.warn("[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。")}const result = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(secretKey), {iv: CryptoJS.enc.Utf8.parse(config.iv || ""),mode: CryptoJS.mode[config.mode || "ECB"],padding: CryptoJS.pad[config.padding || "Pkcs7"]})return result.toString()},decrypt(cipher, secretKey, config={}){const result = CryptoJS.AES.decrypt(cipher, CryptoJS.enc.Utf8.parse(secretKey), {iv: CryptoJS.enc.Utf8.parse(config.iv || ""),mode: CryptoJS.mode[config.mode || "ECB"],padding: CryptoJS.pad[config.padding || "Pkcs7"]})return CryptoJS.enc.Utf8.stringify(result);}}
}export default tool

接着我们进入frontend/src/views/framework/updater目录 (没有创建),创建组件 index.vue ,代码如下

<script setup>
import { ipc } from '@/utils/ipcRenderer';
import { ipcApiRoute, specialIpcRoute } from '@/api/electronApi.js';
import {ref, onMounted} from 'vue';
import { ElMessage } from 'element-plus';
import tool from '@/utils/tool'
// 下载进度条
const progress = ref('');
const percentNumber = ref(0);
const updateTitle = ref('软件更新')
const open = ref(false);
const versionType = ref('')
const percentShow = ref(false)
const dialogTableVisible = ref(false);
onMounted(() => {init()ipc.invoke(ipcApiRoute.framework.getLocalStorage)checkForUpdater()
})const init = () => {ipc.removeAllListeners(specialIpcRoute.appUpdater);ipc.on(specialIpcRoute.appUpdater, (event, result) => {result = JSON.parse(result);//有更新提示信息if (result.status === 3) {progress.value = result.desc;percentNumber.value = result.percentNumber;} else if(result.status === 1){//有可用更新,弹出框if(result.versionType === 'smallUpdate'){ipc.invoke(ipcApiRoute.framework.downloadAsar)}else if(result.versionType === 'largeUpdate'){open.value = true}versionType.value = result.versionType;}else if(result.status === 4){ElMessage({message: '下载已经完成,正准备自动更新',type: 'success',})percentShow.value = true//显示更新提示信息tool.data.set('updateHandleModel','hasUpdate')}else if (result.status === 6) {ElMessage({message: result.desc,type: 'warning',})}})
}function checkForUpdater () {ipc.invoke(ipcApiRoute.framework.checkForUpdater)
}function updateForDownload(){switch (versionType.value){case 'largeUpdate'://大更新,使用全量更新
      ipc.invoke(ipcApiRoute.framework.downloadApp)open.value = falsepercentShow.value = truebreak;// case 'smallUpdate'://   //小更新,使用增量更新//   ipc.invoke(ipcApiRoute.framework.downloadAsar)//  break;default ://没有可用的更新ElMessage.error('没有可用的更新');break;}
}function closeUpdateMsg(){dialogTableVisible.value = false
}function closeUpdateSoftware(){open.value = false
}
</script><template><el-dialog v-model="dialogTableVisible" title="本次更新内容" @closed="closeUpdateMsg"><p>本次软件更新的功能:</p><p>1、优化了windows版本内容</p><p>2、解决了软件安装时没有中文快捷名称</p></el-dialog><div class="home_title_center"><el-progress type="circle" :percentage="percentNumber" v-show="percentShow" :width="80"/></div><el-dialogv-model="open":title="updateTitle"width="30%"@closed="closeUpdateSoftware"><span>检测到当前软件有更新,点击按钮可更新到最新版本</span><template #footer><span class="dialog-footer"><el-button @click="open = false">取消</el-button><el-button type="primary" @click="updateForDownload()">点击更新</el-button></span></template></el-dialog>
</template><style>
.home_title_center {position: absolute;top:20px;right:20px;}
.el-progress__text{color: #fff !important;
}
</style>

frontend/views/example/hello/index.vue中引入组件:

<template><updater></updater><section id="hero"><h1 class="tagline"><span class="accent">Electron-Egg</span></h1><p class="description">A fast, desktop software development framework</p><p class="actions"><a class="setup" href="https://www.kaka996.com/" target="_blank">Get Started</a></p></section>
</template>
<script setup>
import updater from '@/views/framework/updater/index.vue'
console.log("hello")
</script>
<style scoped>
section {padding: 42px 32px;
}#hero {padding: 150px 32px;text-align: center;height: 100%;
}.tagline {font-size: 52px;line-height: 1.25;font-weight: bold;letter-spacing: -1.5px;max-width: 960px;margin: 0px auto;
}
html:not(.dark) .accent,
.dark .tagline {background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);background-clip: text;-webkit-background-clip: text;-webkit-text-fill-color: transparent;
}.description {max-width: 960px;line-height: 1.5;color: var(--vt-c-text-2);transition: color 0.5s;font-size: 22px;margin: 24px auto 40px;
}
.actions a {font-size: 16px;display: inline-block;background-color: var(--vt-c-bg-mute);padding: 8px 18px;font-weight: 500;border-radius: 8px;transition: background-color 0.5s, color 0.5s;text-decoration:none;
}
.actions .setup {color: var(--vt-c-text-code);background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
}
.actions .setup:hover {background-color: var(--vt-c-gray-light-4);transition-duration: 0.2s;
}
</style>

 更改package.json 将version改为2.0.1,然后打包,打包之后在根目录下的out目录会生成几个文件,我们需要将文件上传到七牛云,

image

 然后我们在本地再打包一个低版本的 1.0.1的版本,然后测试是否能够增量更新

测试的效果如下:

image

 

image

 

image

 至此,全量更新就完成了,由于代码有点多,下一章节再讲增量更新

http://www.sczhlp.com/news/1848/

相关文章:

  • iOS WebView 加载失败与缓存刷新问题排查实战指南
  • 数字孪生技术是如何帮助物流行业发展的?
  • ​​Linux PR(Priority)​​ 和 ​​NI(Nice)进程优先级详解
  • 软考系统分析师每日学习卡 | [日期:2025-07-30] | [今日主题:进程管理(一)]
  • c#nopi读取excel内容
  • 容器云网络故障深度排查:POD访问SVC超时全解析
  • 一些图论进阶
  • Django模型关系:从一对多到多对多全解析
  • Higress curl测试Mcp
  • 2025年10款必须知道的项目管理软件推荐,好用的项目管理工具都在这里!
  • 数字时代的隐私盾牌:深度解析Seaoss临时邮箱如何重塑你的网络安全
  • day6
  • 好用的临时邮箱十分钟邮箱推荐(亲测)
  • 临时邮箱、tempmail、十分钟邮箱、24小时邮箱、可丢弃邮箱推荐
  • SSH连接服务器正常显示GUI程序
  • PY_0001:python的安装和打包exe程序
  • 亲测好用的临时邮箱推荐
  • 全链路电商解决方案
  • 智能化婚恋服务解决方案
  • 好用的临时邮箱十分钟邮箱推荐
  • halcon_02_控制结构
  • vue3中组合式api的执行顺序问题
  • 【LeetCode 23】力扣困难算法:合并 K 个升序链表 —— “ 优先队列算法 ”
  • SQL 获取某个作业的最近一次执行时间
  • Higress 案例
  • 基于LVGL 8.4.1,自己实现截图功能,不使用LVGL库函数
  • 360环视技术推荐的硬件平台:支持多摄像头与三屏异显的理想选择
  • 在vue3项目中使用Umo Editor 打包到生产环境报错 Key xxxx must be a async function
  • 钉钉直播回放视频下载工具,如何在电脑端下载钉钉直播回放视频文件到本地?
  • 【IT转码 Day01】