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

 接着上一章节实现了全量更新,现在开始实现增量更新,electron-egg官网增量更新是需要付费的插件,本着不付费的原则,在全量更新的基础上实现一个增量更新。

image

  根据官网,增量更新只更新app.asar 文件的条件,我们只需要把打包安装之后的软件中的app.asar文件替换成最新的就行了,这样就实现了增量更新,所以,在实现增量更新之前,需要在配置中,把 "asar": true, 设置为true, 打开cmd/builder.json 文件。

 同时实现增量更新之前需要使用两个脚本文件,分别对应windows和macos系统下,执行app.asar替换的脚本,这里我只写了windows和macos系统的,其他的可以自行扩展,在build/extraResources

目录下新建bat(自己取名)目录再创建两个文件 replace.sh 和 replace.vbs (这里为什么不是.bat ,因为如果使用bat在执行替换的一瞬间会有命令框的弹出,这样的体验效果不太好,所以使用VBScript脚本语言实现替换就不会导致有命令款弹出),话不多说直接上代码:

replace.sh 代码(代码比较简单,可自行扩展):

#!/bin/bash
sleep 2
#指定要操作的目录
TARGET_DIR=$1
#进入指定目录
cd "$TARGET_DIR" || { echo "无法进入目录 $TARGET_DIR"; exit 1; }

#检查并删除app.asar文件
if [ -f "app.asar" ]; then
    rm "app.asar"
    echo "已删除 app.asar 文件"
else
    echo "app.asar 文件不存在"
fi
#检查并重命名app.asar-temp文件
if [ -f "app.asar-temp" ]; then
    mv "app.asar-temp" "app.asar"
    echo "已将 app.asar-temp 重命名为 app.asar"
else
    echo "app.asar-temp 文件不存在"
fi

replace.vbs 代码:

' 定义等待函数,模拟 timeout 命令
Sub Wait(seconds)
    WScript.Sleep seconds * 1000
End Sub

' 获取命令行参数
Dim args, arg1
Set args = WScript.Arguments
arg1 = args(0)

' 等待1秒
Wait 1

' 尝试删除 app.asar 文件
Dim fso, filePath
Set fso = CreateObject("Scripting.FileSystemObject")
filePath = arg1 & "\app.asar"

If fso.FileExists(filePath) Then
    On Error Resume Next
    fso.DeleteFile filePath, True
    If Err.Number <> 0 Then
        WScript.Echo "Failed to delete app.asar, continuing..."
        Err.Clear
    End If
    On Error GoTo 0
End If

' 尝试重命名 app.asar-temp 为 app.asar
filePath = arg1 & "\app.asar-temp"
Dim newFilePath
newFilePath = arg1 & "\app.asar"

If fso.FileExists(filePath) Then
    On Error Resume Next
    fso.MoveFile filePath, newFilePath
    If Err.Number <> 0 Then
        WScript.Echo "Failed to rename app.asar-temp, continuing..."
        Err.Clear
    End If
    On Error GoTo 0
End If

' 清理对象
Set fso = Nothing

注意:在macos系统中打包:

软件打包安装后如果要在软件内部执行一个.sh文件需要在开发环境打包之前给权限,然后再打包:
chmod +x replace.sh

对于app.asar这个文件,是可以使用工具解决出来源代码的,我们在打包的时候,可以使用严格加密模式,这样就不用担心被别人解压出来源代码,使用electron-egg自带的:

更改 cmd/bin.js文件,将加密部分更改一下:

  /**
   * 加密
   */  
  encrypt: {
    frontend: {
      type: 'strict',
      files: [
        './public/dist/**/*.(js|json)',
      ],
      cleanFiles: ['./public/dist'],
      confusionOptions: {
        compact: true,      
        stringArray: true,
        stringArrayEncoding: ['none'],
        stringArrayCallsTransform: true,
        numbersToExpressions: true,
        target: 'browser',
      }
    },
    electron: {
      type: 'strict',
      files: [
        './public/electron/**/*.(js|json)',
      ],
      cleanFiles: ['./public/electron'],
      specificFiles: [
        './public/electron/main.js',
        './public/electron/preload/bridge.js',
      ],
      confusionOptions: {
        compact: true,      
        stringArray: true,
        stringArrayEncoding: ['rc4'],
        deadCodeInjection: false,
        stringArrayCallsTransform: true,
        numbersToExpressions: true,
        target: 'node',
      }
    }
  },

话不多说,准备工作做完之后直接开搞增量更新了,在 electron/service/os/目录下创建  increment_updater.js ,

 

const { app: electronApp } = require('electron');
const { spawn, exec } = require('child_process');
const { is } = require('ee-core/utils');
const { logger } = require('ee-core/log');
const { getMainWindow, setCloseAndQuit } = require('ee-core/electron');
const Ps = require('ee-core/ps');
const isDev = process.env.NODE_ENV
const path = require('path');
const fs = require('fs');
const http = require('http')
const logPath = electronApp.getPath('logs')
const { LocalStorageService } = require('../LocalStorage');
//获取资源路径
const resourcePath = isDev === 'dev'
    ? process.resourcesPath
    : process.resourcesPath
// 设置app.asar替换文件路径
const localAsarTemp = path.join(resourcePath, 'app.asar-temp')
// 下载文件的地址
const asarUrl = is.windows() ? `http://cos.xxxxx.com/software/windows/ceshi/app.asar`: `http://cos.xxxxx.com/software/macos/ceshi/app.asar`

/**
 * 增量更新
 * @class
 */
class IncrementUpdaterService {

    /**
     * notSuccess | success
     */
    init() {
        let data = {
            downloadSuccess: "notSuccess"
        }
        if (!fs.existsSync(LocalStorageService.getLocalConfigFile())) {
            fs.writeFileSync(LocalStorageService.getLocalConfigFile(), JSON.stringify(data), 'utf8')
            logger.info('配置文件不存在,已自动创建')
        } else {
            logger.info('监测到配置文件')
        }
        return null
    }

    /**
     * 替换 app.asar文件
     * @returns {Promise<void>}
     */
    async replaceAsarFile() {
        let localBatFile
        if((is.windows())){
            localBatFile = path.join(Ps.getExtraResourcesDir(),'./bat/replace.vbs')
        }else if((is.macOS())){
            localBatFile = path.join(Ps.getExtraResourcesDir(),'./bat/replace.sh')
            fs.chmodSync(localBatFile,0o755)
            //给sh文件添加可执行权限
        }
        // fs.chmodSync(localBatFile,0o755)

        const childOut = fs.openSync(path.join(logPath, './out.log'), 'a')
        const childErr = fs.openSync(path.join(logPath, './err.log'), 'a')

        const child = await spawn(`"${localBatFile}"`,[`"${resourcePath}"`], {
            detached: true, // 允许子进程独立 通常与 unref() 方法一起使用,以确保父进程退出时不会等待子进程。
            shell: true,  //当设置为 true 时,命令会在系统的默认 shell(如 cmd.exe 在 Windows 或 /bin/sh 在 Unix 系统)中执行。
            windowsHide:true,
            // stdio: ['ignore', "pipe", "pipe"]
            stdio: ['ignore', childOut, childErr]
        })
        LocalStorageService.setLocalConfig()
        // child.stderr?.on('data', data => logger.info('stderr', data))
        child.unref()
    }

    /**
     * 检测asar文件是否存在,存在则返回文件大小
     * @param path
     */
    async getAsarLength(path) {
        return new Promise((resolve)=>{
            try {
                fs.stat(path,{ sudo: true, admin: true }, (err, stats) => {
                    if (err) {
                        logger.error('获取asar文件大小失败', err)
                        resolve(0)
                    } else {
                        logger.info('获取asar文件大小成功')
                        resolve(stats.size)
                    }
                })
            }catch (e){
                logger.error('获取asar文件大小失败', e)
                resolve(0)
            }
        })
    }

    /**
     * 下载 app.asar
     * @returns {Promise<void>}
     */
    async downloadAsar() {
        this.init()   //配置文件初始化
        const size = await this.getAsarLength(localAsarTemp)
        const fileStream = fs.createWriteStream(localAsarTemp)
        await http.get(asarUrl, res => {
            const headers = res.headers;
            if (Number(headers['content-length']) !== Number(size)) {
                res.pipe(fileStream)
                fileStream
                    .on('finish', () => {
                        LocalStorageService.setLocalConfig('success')

                        //下载完成执行退出
                        setTimeout(() => {
                            const data = {
                                status: 6,
                                desc: '检测到有更新,退出软件时自动更新'
                            }
                            this.sendStatusToWindow(data)
                        },1000)
                    })
                    .on('error', error => {
                        // 删除文件
                        fs.unlink(localAsarTemp, () => {
                            logger.error('下载出现异常,已中断', error)
                        })
                    })
            } else {
                res.resume();
                LocalStorageService.setLocalConfig('notSuccess')
            }
        })
    }

    /**
     * 向前端发消息
     */
    sendStatusToWindow(content = {}) {
        const textJson = JSON.stringify(content);
        const channel = 'custom/app/updater';
        const win = getMainWindow();
        win.webContents.send(channel, textJson);
    }
}

IncrementUpdaterService.toString = () => '[class IncrementUpdaterService]';

const incrementUpdaterService = new IncrementUpdaterService();

module.exports = {
    IncrementUpdaterService: incrementUpdaterService
};

修改 controller中的 framework.js,添加代码:


const {app: electronApp} = require("electron");
const { LocalStorageService } = require('../service/LocalStorage');
const { IncrementUpdaterService } = require('../service/os/increment_updater');
//类里面添加一个方法
//这个方法下载远程app.asar文件
downloadAsar() {
  IncrementUpdaterService.downloadAsar();
}
//在底部添加主进程退出监听
/**
* 监听桌面退出
*/
electronApp.addListener('quit',(event)=>{
// logger.info('应用程序退出开始执行删除操作')
if(LocalStorageService.getLocalConfig().downloadSuccess === 'success'){
IncrementUpdaterService.replaceAsarFile();
//新增更新记录弹窗
// localStorage.setItem('updateHandleModel', 'hasUpdate')
LocalStorageService.setLocalStorage({
updateHandleModel: 'hasUpdate'
})
}
})

FrameworkController.toString = () => '[class FrameworkController]';

修改 frontend/src/views/framework/updater/index.vue 文件,修改代码:

//修改init方法
const init = () => {
  ipc.removeAllListeners(specialIpcRoute.appUpdater);
  ipc.on(specialIpcRoute.appUpdater, (event, result) => {
    result = JSON.parse(result);
    console.log(result)
    //有更新提示信息
    if (result.status === 5 && result.localStorageMsg && result.localStorageMsg === 'hasUpdate') {
      dialogTableVisible.value = true
      ipc.invoke(ipcApiRoute.framework.setLocalStorage)
    }else 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.value.data.set('updateHandleModel','hasUpdate')
    }else if (result.status === 6) {
      ElMessage({
        message: result.desc,
        type: 'warning',
      })
    }
  })
}

之后打包桌面软件,先打包一个2.0.2的版本,安装好之后将生成好的app.asar上传到云空间,然后把 latest.yml ee-win-2.0.2-x64.exe.blockmap 这两个文件一起上传上去,再打包一个2.0.1的版本本地安装,打开桌面软件测试:

image

 

image

image

 

已经下载好了临时文件 退出时会自动替换文件,至此增量更新完成

posted @ 2025-07-31 10:40  来碗酒喝  阅读(211)  评论(2)    收藏  举报