Electron - 自动升级总结

1. 概述

本文介绍electron应用的在线升级方案调研和开发。

2. 客户端升级方案探讨

客户端升级方案

如上图,通过三个方面讨论一下在线升级的几个概念

2.1. 带宽因素

带宽的大小,直接关系到安装包的下载时长。
带宽因素,其实也就是安装包的体积因素,所以,尽量生成高压缩比的安装包。
Electron应用的package.jsonbuild字段,可配置安装包的高压缩比:

{
  "build": {
    "asar": true,
    "compression": "maximum",
    ... 
  }
}

在安装包大小一定的情况下,在线升级分为全量更新增量更新

全量更新

下载NewVersion的Installer,并覆盖安装

增量更新

  • 方法1,每次Release需要生成与历史所有版本的diff文件,当Client发起Update请求时,对比OldVersion和NewVersion的差别文件,并打包Response给Client
  • 方法2,基于Release包打包经常变动的文件作为增量升级包,例如app.asar。于是,如果更新了其他文件的话,只能强制全量更新
  • 方法3,对比installer单文件差异,然后只下载差异block即可
  • 结论:开发麻烦,增加了在线升级的复杂度,性价比低,项目前期不建议

Electron应用如何支持增量更新

Electron-updater采用上述方法3,更新机制和详细日志可以参考附1的日志

2.2. 用户量

对于活跃用户量很大的应用,灰度发布很有必要,不论客户端还是服务端
如果是项目初期,用户量并不多,功能并不复杂,变动大,而且经常迭代更新,全量发布会减少项目的迭代负担,更早发现问题、解决问题
当然,前提是测试要充分,让用户发现问题是下下策

全量发布

针对所有用户发布

灰度发布

只针对部分用户发布,可以随机20%发布,也可以基于用户Tag发布(前提是有可用的用户Tag)

Electron应用如何支持灰度发布

Electron应用在electron-updater的基础上,也可以支持灰度发布,三个点:

  • latest.yml增加自定义字段,比如grayReleased: 80
  • 客户端每次启动,自动获取是否有可用的update,以及其详细信息,即latest.yml
  • 客户端本地随机生成100以内的自然数,如果小于80,则提示升级或自动升级,否则不升级

2.3. 兼容性

强制更新

启动时,自动无感更新

这种情况,一般是服务端的升级已不再支持旧版本,Electron应用的实现机制

  • latest.yml记录服务端支持的最低版本,例如minVersionSupported: 0.1.0
  • 客户端每次启动,自动获取是否有可用的update,以及其详细信息,即latest.yml
  • 客户端比较Current VersionminVersionSupported,决定是否强制更新

可选更新

启动时,自动可选更新

强制更新的最后一步,如果CurrentVersion >= minVersionSupported,用户可选择是否立即更新

启动后,手动检测更新

一般情况下,在客户端会提供一个Page,供用户手动查看当前版本、检测是否有新版本,以及执行立即更新

2.4. 其他问题

2.4.1. 新版本的变更内容

仿照灰度发布强制更新的做法,可以修改latest.yml来实现

Electron支持Github Release的Release-Notes,对其他server不支持,所以只能用上面workaround

2.4.2. 客户端安装失败后怎么办

这个问题很严重,主要是对终端的适配问题,于是,测试工作就显得尤为重要

如果真在用户端发生了,需要具体问题具体分析了

  • 如何及时捕获到异常,并告知到服务端
  • 如何回滚到旧版本,让用户先用着

3. 构建与发布

3.1. 打包工具

Electron应用的打包和发布,主流工具有3个,可以参考官网介绍

经过各种对比,主要是易用性和生态完整性,决定采用electron-builder,官网:https://www.electron.build

3.2. 自动更新工具

Differences between electron-updater and built-in autoUpdater

  • Dedicated release server is not required.
  • Code signature validation not only on macOS, but also on Windows.
  • All required metadata files and artifacts are produced and published automatically.
  • Download progress and staged rollouts supported on all platforms.
  • Different providers supported out of the box (GitHub Releases, Amazon S3, DigitalOcean Spaces, Bintray and generic HTTP(s) server).
  • You need only 2 lines of code to make it work.
import { autoUpdater } from "electron-updater"
autoUpdater.checkForUpdatesAndNotify()

Electron-updater的优缺点:

优点

  • 版本更新检查基于package.json中的version字段值,支持三段式的版本号,无法定制
  • 包含在线升级功能,实现了版本检查逻辑及回调、下载安装升级逻辑
  • 支持多种版本服务器,包括自搭建的通用服务器
  • 支持alpha/beta/latest版本更新策略
  • 支持增量更新,实现了增量更新失败时转全量更新的策略(目前只有windows版的,项目组据说计划增加mac版本的增量更新)
  • 支持构建时自动发布版本到服务器
  • *支持降级更新

缺点

3.3. Release Server

Electron的打包最终是为了发布,因此支持不同方式的Publish,Release Server有以下类型:

  • Bintray
  • Generic Server, any HTTP(S) server, such as electron-release-server
  • GitHub
  • S3, AWS Object Storage Server
  • Spaces or Snap Store, the app store for Linux

electron-release-server是一个完善的Generic Server,支持安装包的上传和下载,以及分类管理,自带Web。缺点是,还需要自己手动upload,不能自动publish。

本文采用MinIO(类S3的分布式对象存储服务)为例。

4. 打包开发与测试

4.1. 启动minio server

docker run -p 9999:9000 \
  --name minio --rm -d \
  -e "MINIO_ROOT_USER=xxx" \
  -e "MINIO_ROOT_PASSWORD=wJalrXUtnFENG/bxxR12CYxxxwwEXAMPLEKEY" \
  -v /home/xxx/soft/minio/data:/data \
  minio/minio server /data

通过浏览器打开MinIO Dashboard:http://10.211.28.93:9999
并创建一个prefix是*的readonly的bucket,名字是public,下面会用到

4.2. 自动打包发布

修改package.json,增加build字段如下,electron-builder会读这个字段

增加electron-builder.env文件,内容如下,存储MinIO的秘钥

# publish to minio witch is s3-likely storage server
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=wJalrXUtnFENG/bxxR12CYxxxwwEXAMPLEKEY

执行yarn electron-builder --publish always即可打包和发布

  • publish[0].path是auto-updater默认的feedurl,不能包含${version},必须指向最新版本的latest.yml和安装包

  • publish[1]会自动上传安装包到MinIO,主要用于备份

  • publish[2]只是生成了latest.yml,并不会自动上传,需要自动上传功能,需要通过js调用electron-builder实现。可手动move到http-server的目录下即可,也可以采用electron-release-server

推荐采用MinIO作为release-server,可以自动publish,提高效率

{
  "build": {
    "appId": "com.foxchat.www",
    "asar": true,
    "compression": "maximum",
    "directories": {
      "output": "release/${version}"
    },
    "electronDownload": {
      "mirror": "https://npm.taobao.org/mirrors/electron/"
    },
    "files": [
      "!node_modules",
      "node_modules/@sentry/**/*",
      "dist/**"
    ],
    "mac": {
      "icon": "build/icons/icon.icns",
      "artifactName": "${productName}-${os}-${arch}-${version}-setup.${ext}",
      "target": [
        "dmg"
      ]
    },
    "win": {
      "icon": "build/icons/logo256.ico",
      "artifactName": "${productName}-${os}-${arch}-${version}-setup.${ext}",
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ]
    },
    "linux": {
      "icon": "build/icons",
      "target": "deb"
    },
    "nsis": {
      "oneClick": false,
      "allowElevation": true,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "./build/icons/logo256.ico",
      "uninstallerIcon": "./build/icons/logo256.ico",
      "installerHeaderIcon": "./build/icons/logo256.ico",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "FoxChat",
      "deleteAppDataOnUninstall": false
    },
    "publish": [
      {
        "provider": "s3",
        "bucket": "public",
        "endpoint": "http://10.211.28.93:9999",
        "channel": "latest",
        "path": "/im/artifact/latest/${os}"
      },
      {
        "provider": "s3",
        "bucket": "public",
        "endpoint": "http://10.211.28.93:9999",
        "channel": "latest",
        "path": "/im/artifact/${version}/${os}"
      },
      {
        "provider": "generic",
        "url": "http://10.211.28.93:9797/artifact"
      }
    ]
  },
}

4.3. Tips

修改nsis脚本,可以自定义安装界面

参考:https://github.com/eyasliu/blog/issues/22

image
image

5. 客户端自动更新开发

基于electron-updater完成开发,直接上代码

这里补充几点:

  • app.getVersion()不work的话,可以这样子
import { version } from '../../../package.json'
import { app } from 'electron'
app.getVersion = () => version
  • 代码里面的info,就是latest.yml的数据,样例如下
{
  version: '0.0.7',
  files: [
    {
      url: 'FoxChat-win-0.0.7-setup.exe',
      sha512: '795pdq9/Lt9MuANpM3tChxpCtzsqNe4v2wWyK41kUJ3PABHJns2xo5Jw46CWFOOCEVes57L5lmXV9v4NWNBeXg==',
      size: 62858586
    }
  ],
  path: 'FoxChat-win-0.0.7-setup.exe',
  sha512: '795pdq9/Lt9MuANpM3tChxpCtzsqNe4v2wWyK41kUJ3PABHJns2xo5Jw46CWFOOCEVes57L5lmXV9v4NWNBeXg==',
  releaseDate: '2021-06-29T10:09:27.716Z',
  releaseNotes: [ '增加在线升级功能', '登录功能优化' ],
  minVersionSupported: '0.1.0',
  grayReleased: 100,
  downloadedFile: 'C:\\Users\\Administrator\\AppData\\Local\\foxchat-updater\\pending\\FoxChat-win-0.0.7-setup.exe'
}
自动升级的安装包下载路径:
C:\Users\Administrator\AppData\Local\foxchat-updater

Electron-log的默认路径
C:\Users\Administrator\AppData\Roaming\FoxChat\logs
  • TODO,开发一个UI,提高自动升级功能的用户体验
import { app, dialog } from 'electron'
import { autoUpdater } from "electron-updater"
import log from 'electron-log'

log.transports.file.level = "debug"
autoUpdater.logger = log

function updateHandle() {
    log.info('client version', app.getVersion())

    autoUpdater.autoDownload = false

    autoUpdater.on('error', (error) => {
        dialog.showErrorBox('Error: ', error == null ? "unknown" : (error.stack || error).toString())
    })
    
    autoUpdater.on('checking-for-update', () => {
        log.info('Checking for update')
    });

    autoUpdater.on('update-available', (info) => {
        log.info('Got a new client version, will auto download it', info)
        autoUpdater.downloadUpdate()  
    })

    autoUpdater.on('update-not-available', (info) => {
        log.info('Current version is up-to-date', info)
    })

    autoUpdater.on('update-downloaded', (info) => {
        log.info(info)
        dialog.showMessageBox({
            type: 'info',
            title: '软件升级',
            message: '发现新版本,是否立即升级?',
            buttons: ['是的', '稍后']
        }).then((resp) => {
            if (resp.response === 0) {
                log.info('begin to install new version ...')
                autoUpdater.quitAndInstall(true, true)
            }
        })
    })

    autoUpdater.on('download-progress', function (progressObj) {
        log.debug('download progress', progressObj)
    })

    autoUpdater.checkForUpdates()
}

updateHandle()

export {updateHandle}

附1,electron-updater的增量更新日志

增量更新的机制

  • 对比old-version.exe.blockmap文件和new-version.exe.blockmap文件,计算得到changed blocks
  • 基于changed blocks,计算得到download blockskind=1是需要下载的(change-blocks肯定要大于download-blocks)
  • 下载所有kind=1download blocks,并与本地文件merge,形成available installer

效果

从下面的log可以,可以看到只下载了7%大小的文件内容,体积不到4MB:Full: 61,385.34 KB, To download: 4,103.49 KB (7%)

一次客户端升级的完整日志

[2021-06-30 15:27:03.799] [info]  client version 0.0.6
[2021-06-30 15:27:03.807] [info]  Checking for update
[2021-06-30 15:27:13.150] [info]  Found version 0.0.7 (url: FoxChat-win-0.0.7-setup.exe)
[2021-06-30 15:27:13.151] [info]  Got a new client version, will auto download it {
  version: '0.0.7',
  files: [
    {
      url: 'FoxChat-win-0.0.7-setup.exe',
      sha512: '795pdq9/Lt9MuANpM3tChxpCtzsqNe4v2wWyK41kUJ3PABHJns2xo5Jw46CWFOOCEVes57L5lmXV9v4NWNBeXg==',
      size: 62858586
    }
  ],
  path: 'FoxChat-win-0.0.7-setup.exe',
  sha512: '795pdq9/Lt9MuANpM3tChxpCtzsqNe4v2wWyK41kUJ3PABHJns2xo5Jw46CWFOOCEVes57L5lmXV9v4NWNBeXg==',
  releaseDate: '2021-06-29T10:09:27.716Z'
}
[2021-06-30 15:27:13.155] [info]  Downloading update from FoxChat-win-0.0.7-setup.exe
[2021-06-30 15:27:13.157] [debug] updater cache dir: C:\Users\Administrator\AppData\Local\foxchat-updater
[2021-06-30 15:27:13.164] [info]  Download block maps (old: "http://10.211.28.93:9999/public/im/artifact/latest/win/FoxChat-win-0.0.6-setup.exe.blockmap", new: http://10.211.28.93:9999/public/im/artifact/latest/win/FoxChat-win-0.0.7-setup.exe.blockmap)
[2021-06-30 15:27:13.202] [info]  File has 195 changed blocks
[2021-06-30 15:27:13.203] [debug] [
  {
    "kind": 0,
    "start": 0,
    "end": 350804
  },
  {
    "kind": 1,
    "start": 350804,
    "end": 430577
  },
  {
    "kind": 0,
    "start": 430577,
    "end": 463345
  },
  {
    "kind": 1,
    "start": 463345,
    "end": 490212
  },
  {
    "kind": 0,
    "start": 490212,
    "end": 10750880
  },
  {
    "kind": 1,
    "start": 10750880,
    "end": 14595768
  },
  {
    "kind": 0,
    "start": 14595640,
    "end": 54052515
  },
  {
    "kind": 1,
    "start": 54052643,
    "end": 54172009
  },
  {
    "kind": 0,
    "start": 54171859,
    "end": 62463158
  },
  {
    "kind": 1,
    "start": 62463308,
    "end": 62479157
  },
  {
    "kind": 0,
    "start": 62479006,
    "end": 62743206
  },
  {
    "kind": 1,
    "start": 62743357,
    "end": 62858586
  }
]
[2021-06-30 15:27:13.220] [info]  Full: 61,385.34 KB, To download: 4,103.49 KB (7%)
[2021-06-30 15:27:13.224] [info]  Differential download: http://10.211.28.93:9999/public/im/artifact/latest/win/FoxChat-win-0.0.7-setup.exe
[2021-06-30 15:27:13.249] [debug] download range: bytes=350804-430576
[2021-06-30 15:27:13.266] [debug] download progress {
  total: 4201972,
  delta: 79773,
  transferred: 79773,
  percent: 1.8984657679775117,
  bytesPerSecond: 1899357
}
[2021-06-30 15:27:13.268] [debug] download range: bytes=463345-490211
[2021-06-30 15:27:13.278] [debug] download progress {
  total: 4201972,
  delta: 106640,
  transferred: 106640,
  percent: 2.5378560352139425,
  bytesPerSecond: 1974815
}
[2021-06-30 15:27:13.388] [debug] download range: bytes=10750880-14595767
[2021-06-30 15:27:13.900] [debug] download progress {
  total: 4201972,
  delta: 3951528,
  transferred: 3951528,
  percent: 94.03984605323406,
  bytesPerSecond: 5845456
}
[2021-06-30 15:27:16.128] [debug] download range: bytes=54052643-54172008
[2021-06-30 15:27:16.138] [debug] download progress {
  total: 4201972,
  delta: 3964841,
  transferred: 3964841,
  percent: 94.3566734856872,
  bytesPerSecond: 1361085
}
[2021-06-30 15:27:16.156] [debug] download progress {
  total: 4201972,
  delta: 106053,
  transferred: 4070894,
  percent: 96.88055988949951,
  bytesPerSecond: 1388436
}
[2021-06-30 15:27:16.218] [debug] download range: bytes=62463308-62479156
[2021-06-30 15:27:16.236] [debug] download progress {
  total: 4201972,
  delta: 121902,
  transferred: 4086743,
  percent: 97.2577399373437,
  bytesPerSecond: 1356820
}
[2021-06-30 15:27:16.240] [debug] download range: bytes=62743357-62858585
[2021-06-30 15:27:16.263] [debug] download progress {
  total: 4201972,
  delta: 237131,
  transferred: 4201972,
  percent: 100,
  bytesPerSecond: 1382682
}
[2021-06-30 15:27:16.634] [info]  New version 0.0.7 has been downloaded to C:\Users\Administrator\AppData\Local\foxchat-updater\pending\FoxChat-win-0.0.7-setup.exe
[2021-06-30 15:27:32.884] [info]  begin to install new version ...
[2021-06-30 15:27:32.885] [info]  Install on explicit quitAndInstall
[2021-06-30 15:27:32.886] [info]  Install: isSilent: true, isForceRunAfter: true
[2021-06-30 15:27:33.149] [info]  Update installer has already been triggered. Quitting application.
[2021-06-30 15:27:43.027] [info]  client version 0.0.7
[2021-06-30 15:27:43.210] [info]  App starting... 0.0.7
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容