electron怎么在安装完成后更新sqlite_客户端


背景介绍

Electron 客户端是以前端技术栈为主,由于前端技术的迭代更新速度比较快,所以为了提高和优化应用的性能,我们的应用也需要不断升级。并且业务需求的不断增加,应用也会面临重新发版升级的问题。当我们发版更新比较频繁时,如果升级的方式不够友好、不够优雅,那么对用户的体验就会造成很大影响。 所以要考虑各种情况,选择比较优雅的升级方法。本文主要围绕以electron-builder打包方式打包应用时,如何实现应用更新而展开,希望对大家能够有所帮助。

应用UI层更新方案

Electron应用是由主进程和渲染进程组成。主进程主要为web应用提供native能力,而渲染进程负责UI交互。在一些业务场景下主进程代码没有改动, 只是渲染进程代码有更新,因此客户端只需更新渲染进程即可,也就是主进程创建的window窗口只需要重新加载新的html页面即可,具体流程如下。

更新步骤(如图1-1)

  1. 客户端主进程通过轮询、定时请求或者服务端推送方式接收更新通知
  2. 在主进程中对需要更新的渲染进程执行 webContents.reloadIgnoringCache()完成页面重载


electron怎么在安装完成后更新sqlite_electron打包_02


图1-1

应用全量升级解决方案

当应用代码改动较大时,比如Electron版本升级、项目架构调整等,我们就可能需要用户下载全量的升级包来升级。Electron 官方提供了多种应用更新方案 参考链接,主要包括使用Electron 团队维护的update.electronjs.org实现自动更新,以及electron-builder打包方案中的electron-updater。我们下问介绍的就是使用electron-updater完成应用的全量升级。

electron-builder 打包配置

在打包配置文件中,需要配置publish选项,指定provider和url, 其中url是存放升级更新包的服务器。

打包配置文件


{
    "appId": "com.test.app",
    "productName": "APP",
    "files": [
        "./build/**/*"
    ],
    "compression": "maximum",
    "directories": {
        "output": "dist"
    },
    "publish": [
        {
            "provider": "generic",
            "url": "http://static.test.com"  >> 资源服务器
        }
    ],
    "mac": {
        "target": [
            "dmg",
            "zip"
        ]
    }
}


打包生成的文件

打包完成之后,我们将打包目录下生成的所有文件都放到资源服务器,给应用提供下载。


├── mac
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
├── latest-mac.yml


latest-mac.yml文件内容(图1-2)


electron怎么在安装完成后更新sqlite_客户端_03


图1-2

更新步骤(如图1-3,1-4)

  1. 客户端通过定时检测、或者服务端推送方式检测是否有更新
  2. 执行autoUpdater.checkForUpdates()的检测逻辑,读取资源服务器latest-mac.yml文件,对比文件hash摘要
  3. 有更新则执行文件下载操作,可以配合UI层显示下载进度
  4. 下载完毕之后,通知UI层并显示本次更新的相关内容
  5. 应用重启进行更新


electron怎么在安装完成后更新sqlite_客户端_04


图1-3


electron怎么在安装完成后更新sqlite_客户端_05


图1-4

electron-updater更新参考代码


import { autoUpdater } from 'electron-updater';

    // 接收应用层通知,是否需要进行版本更新检查
    ipcMain.on('update-version-now', () => {
        // 调用更新检测方法, 会触发下边注册好的事件
        autoUpdater.checkForUpdates();
    });

    // 接收应用层确认更新的通知, 调用自动退出,并且完成更新
    ipcMain.on("update-version-confirm", () => {
        autoUpdater.quitAndInstall();
        app.quit();
    });

    // 设置更新服务器url
    autoUpdater.setFeedURL(feedURL);

    // 监听error
    autoUpdater.on("error", function(error) {
        mainWindow.webContents.send("update-version-error");
    });

    // 检测开始
    autoUpdater.on("checking-for-update", function() {
        mainWindow.webContents.send("checking-for-update");
    });

    // 更新可用
    autoUpdater.on("update-available", function(info) {
        mainWindow.webContents.send("update-available");
    });

    // 更新不可用
    autoUpdater.on("update-not-available", function(info) {
        mainWindow.webContents.send("update-not-available");
    });

    // 更新下载进度事件
    autoUpdater.on("download-progress", function(progress) {
        mainWindow.webContents.send("download-progress", progress);
    });

    // 更新下载完毕
    autoUpdater.on('update-downloaded', function () {
        //下载完毕,通知应用层 UI
       mainWindow.webContents.send('update-version-downloaded');
    });


全量更新方案的优缺点

优点

  1. 打包配置简单,只需添加 publish 即可
  2. 代码逻辑简单,添加 autoUpdater 逻辑即可

缺点

  1. 安装包体积过大时,浪费带宽,增加用户升级时间
  2. 代码改动量小时,全量升级完全没有必要
  3. 当我们的应用依赖一些第三方SDK时,当第三方SDK有打包问题时,可能会出现升级失败的情况

应用增量更新升级解决方案

应用在代码改动较少的情况下,用户体验好、比较优雅的更新方式就是增量更新了。增量更新的方案也有多种,具体的增量更新方案需要针对具体的业务需求进行定制。下边介绍2种常见的增量更新方案,我们仍然是基于electron-builder的打包方式来实现的。

方案一: 固定模块升级(使用asar代码压缩)

asarElectron提供的一种将多个文件合并成一个文件的类tar风格的归档格式,不仅可以缓解Windows下路径名过长的问题, 还能够略微加快一下 require的速度, 并且可以隐藏你的源代码( 并非绝对隐藏,专业人士还是可以解压缩的

asar方式下应用的启动流程(如图1-5)

要想完成 asar方式下应用的更新,我们必须先了解Electron应用在这种模式下是如何启动。 其实在这种模式下Electron应用在启动时 会读取app.asar.unpacked目录中的内容合并到app目录中进行加载,因此增量更新时我们只需要替换应用安装目录中的app.asar.unpacked目录,然后重启应用即可。


electron怎么在安装完成后更新sqlite_客户端_06


图1-5

基于electron-builderasar打包配置

了解了asar方式下应用的启动流程之后,我们就可以对electron-builder的打包配置文件进行修改,设置成asar模式, 具体的配置文件如下。


{
    "appId": "com.test.app",
    "productName": "APP",
    "files": [
        "./build/**/*"
    ],
    "compression": "maximum",
    "asar": true,
    "asarUnpack": [
        "./build/src",   >> 不需要打包到asar中的文件,也就是有改动的代码
        "./build/sdk/one",
        "./build/sdk/two"
    ],
    "directories": {
        "output": "dist"
    },
    "mac": {
        "target": [
            "dmg",
            "zip"
        ]
    }
}


打包生成文件

根据配置文件打包后会生成安装包和增量包,其中 app.asar压缩文件就是基本不要变动的代码,app.asar.unpacked 目录就是我们的增量文件,也就是需要变更的代码。 最后注意我们要对增量包进行压缩,减少更新包体积,然后上传文件到服务器。


├── mac
    ├── Contents
        ├── _CodeSignature
        ├── Frameworks
        ├── MacOS
        ├── Resources
            ├── app.asar  >> 这就是我们的asar包,也就是不需要改动的代码
            ├── app.asar.unpacked  >> 这就是我们的增量包,修改更新的代码
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
└── latest-mac.yml


更新过程(如图1-6)

  1. 客户端通过定时检测、或者服务端推送方式检测是否有更新
  2. 通过版本比对发现更新,并获取到需要更新的文件名称
  3. 下载 app.asar.unpacked.xxx.tgz 更新文件到应用的指定目录(路径因系统而异)
  4. 解压覆盖原文件,重启应用


electron怎么在安装完成后更新sqlite_客户端_07


图1-6

应用安装路径示例

MacOS /Applications/App.app/Contents/Resources

Windows D:/xxx/App/resources

更新过程参考代码


//下载更新文件
    fetch(downloadURL).then(res => {
        let stream = fs.createWriteStream(unpackPath);
        res.body.pipe(stream);
        stream.on('close', () => {
            //执行解压更新操作
            uncompressAndUpdate();
        });
    }).catch(err => {
        logger.error(`download ${downloadURL}: ${err.toString()}`);
    });

    function uncompressAndUpdate ()  {
       //先备份当前的 app.asar.unpacked目录
       fs.renameSync(untgzPath, `${untgzPath}.back`);

       compressing.tgz.uncompress(unpackPath, appPath).then(res => {
           logger.info(`uncompress ${asarName} success`);
           deleteDirSync(`${untgzPath}.back`);

           //解压之后,重启应用即可
           app.relaunch();
           app.exit(0);
       }).catch(err => {
           //记录错误日志
           logger.error(`uncompress ${asarName} error: ${err.toString()}`);
           fs.renameSync(`${untgzPath}.back`, untgzPath);
       });
    }


asar 方式的优缺点

优点

  1. 可以对代码进行压缩,在一定程度上隐藏源码、提高加载速度
  2. asarasarunpacked 分开, 很方便的实现增量更新

缺点

  1. 只能在一定程度上隐藏源码,使用 asar 可以方便的解压缩(可靠的方法还是对源码进行混淆、压缩)
  2. asar压缩文件中存在 Node API 的局限性, 无法实现非压缩下的所用功能,对于有对Node执行有强需求的可能要仔细斟酌该方案, 笔者就遇到了无法执行 child_process.exec,child_process.spawn方法的问题

方案二: 自定义模块升级(非asar方式)

使用非 asar方式,可以让我们的应用拥有更多的灵活性。下边我们介绍的方式,就是充分利用了electron-builder中两个常用的配置选项extraResources(拷贝资源到打包目录Resources中)、extraFiles(拷贝资源到打包目录的根路径), 详细文档 帮助我们轻松实现增量更新。

基于electron-builder的非asar打包配置文件


{
    "appId": "com.test.app",
    "productName": "App",
    "files": [
        "./build/**/*"
    ],
    "asar": false,
    "asarUnpack": [],
    "compression": "maximum",
    "directories": {
        "output": "dist"
    },
    "mac": {
        "target": [
            "dmg",
            "zip"
        ],
        "icon": "./build/src/icons/app.icns",
        "extendInfo": {
            "CFBundleURLSchemes": [
                "schema"
            ]
        },
        "extraResources": [ >> 拷贝需要的资源
            {
                "from": "./SDK/",
                "to": "SDK"
            }
        ]
    }
}


根据配置打包,生成安装包

打包完成生成如下的文件。

app目录是我们的应用目录。 SDK目录就是我们额外拷贝过去的目录。 SDK 都属于改动较小的部分,而我们的app是改动比较频繁的目录。因此增量更新其实就是替换app目录即可,而完全不需要重新下载SDK,当然如果SDK也需要更新的话,更新逻辑中可以添加SDK的更新即可。 在打包完成之后,使用压缩脚本自动将 app目录压缩生成 app_mac_0.0.2.tgz,它就是我们的更新包,上传到更新服务器。


├── mac
    ├── Contents
        ├── _CodeSignature
        ├── Frameworks
        ├── MacOS
        ├── Resources
            ├── app  >> 应用代码
            ├── SDK  >> 应用代码中依赖的SDK,被拷贝过来的
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
└── latest-mac.yml


asar增量更新代码流程(图1-7)

  1. 客户端定时、轮询,或者服务端push方式通知检测更新
  2. 从服务器下载需要更新的文件
  3. 解压并覆盖已有的文件
  4. 退出重启应用


electron怎么在安装完成后更新sqlite_客户端_08


图1-7

升级参考代码(和以上的方式类似),只列举了app的更新


//下载更新文件
    fetch(appURL).then(res => {
        let stream = fs.createWriteStream(appTgzPath);
        res.body.pipe(stream);
        stream.on('close', () => {
            uncompressAndUpdate();
        });
    }).catch(err => {
        logger.error(`download ${appURL}: ${err.toString()}`);
    });

    function uncompressAndUpdate ()  {
        //先备份当前的 app.asar.unpacked目录
        fs.renameSync(appUnTgzPath, `${appUnTgzPath}.back`);

        compressing.tgz.uncompress(appTgzPath, appPath).then(res => {
            logger.info(`uncompress ${appTgzName} success`);
            deleteDirSync(`${appUnTgzPath}.back`);
            app.relaunch();
            app.exit(0);
        }).catch(err => {
            logger.error(`uncompress ${appTgzName} error: ${err.toString()}`);
            fs.renameSync(`${appUnTgzPath}.back`, appUnTgzPath);
        });
    }


asar方式的优缺点

优点

  1. 不用考虑asar的各种限制
  2. 更新方式极其灵活,可根据业务方便的进行定制

缺点

  1. 无法对代码进行归档压缩(其实对源码对混淆压缩才是真的隐藏)

常用组合

由于项目的不断迭代更新,我们的项目会有不同的升级需求, 既可能有全量更新的版本,又可能有增量更新的版本, 所以我们的应用要同时支持这两种方式。经过以上几种方式的分析对比,我们只需要把全量更新的方案和非asar方案组合到一起即可, 只要添加完善的更新检测,区分执行哪一种即可轻松搞定。

参考electron-builder配置


{
    "appId": "com.test.app",
    "productName": "APP",
    "files": [
        "./build/**/*"
    ],
    "asar": false, >> 配合实现增量更新
    "compression": "maximum",
    "directories": {
        "output": "dist"
    },
    "publish": [
        {
            "provider": "generic",  >> 完成全量更新
            "url": "http://static.test.com"
        }
    ],
    "mac": {
        "target": [
            "dmg",
            "zip"
        ],
        "icon": "./build/src/icons/app.icns",
        "extendInfo": {
            "CFBundleURLSchemes": [
                "meishiim"
            ]
        },
        "extraResources": [
            {
                "from": "./SDK/",
                "to": "SDK"
            }
        ]
    }
}


应用的整体更新架构(图1-8)


electron怎么在安装完成后更新sqlite_客户端_09


图1-8

自定义的更新检测逻辑(图1-9)

  1. 客户端定时、轮询,或者服务端push方式通知检测更新(包含灰度)
  2. 根据package.json文件获取要更新的文件
  3. 从服务器上下载所有更新文件
  4. 解压并覆盖原文件
  5. 退出重启应用


electron怎么在安装完成后更新sqlite_electron增加页面返回_10


图1-9

总结

通过对以上几种更新方案的介绍,相信大家对 Electron应用更新应该有了一个更加直观的认识。每一种方案都有其适用的应用场景,选择哪一种则需要根据具体的业务需求来综合考虑。以上的更新方案都是笔者根据业务需求探索总结的,如果有其他方案,可以共同探讨~