Electron 中的构建产物 blockmap


前言

electron-builder 构建出来的产物,默认都会有个 .blockmap 文件,这个文件是用来做增量更新的。

什么作用

实现差异检测,增量更新,减少下载量,提高下载速度。

实现逻辑

  • 只适用于 nsis, appImage 安装方式
  • 在构建阶段输出 blockmap 文件,是当前文件的分块内容描述,文件本身是种文本文件的编码,用于区间下载(指定范围),以及文件复制操作(指定范围)
  • 运行阶段, 以 nsis 为例,触发更新时,拉取 latest.yml 后, 会检测即将更新的文件对应的 .blockmap 文件, 并尝试使用 blockmap ,逻辑如下:
    • 获取到即将更新的文件的 blockmap 文件 url
    • 通过 version 替换,得到当前版本对应的 blockmap 文件 url
    • 检测本地是否有当前版本更新使用的安装文件 (自动更新生成的, 在 Cache 目录下的那个文件)
    • 通过对比 oldBlockmap 和 newBlockmap,得到需要更新的文件的分块操作信息, 有两个类型, COPY 和 DOWNLOAD, 分别包含对应操作的分块区间
    • 通过得到的分块操作信息,在内存中生成新的安装文件,逻辑就是创建文件流,一部分从老安装文件 COPY, 一部分从网络 DOWNLOAD, 最后生成新的安装文件
    • 一切顺利,就得到新的安装包了,通过 blockmap 的方式,实现了增量下载,按需下载能力,是静态服务器支持 multipart/byteranges 请求的头
    • 上述过程中,还有很多 sha256 的校验,以及异常处理, 如果发生失败, 都会走到全量下载方式上

几个注意事项:

  • blockmap 是 electron-builder 的更新的优化方案, 不是 electron 默认支持的 autoUpdater 的更新方案
  • NSIS 的话,记得要上传安装文件和安装文件对应的 blockmap 文件。并确保 blockmap 文件各版本的 url 是可访问的
  • Mac 生成 zip 的时候,会强制生成 blockmap 文件,不可禁用,但又不会被使用
  • NSIS 如果不使用 blockmap, 可在 electron-builder 中配置 "nsis":{ "differentialPackage": false}

源码部分

Code search results

构建时候

Code search results

如果是第一次构建,这个 .blockmap 生成就代表了这个对应文件的差分部分的内容。

运行阶段

  • 本地要有 blockmap 文件, 然后有远程的 blockmap 文件
  • 计算出要下载远程的 zip 的哪一部分,下载文件时,是可以下载特定区间的,比如:文件内容是 a 到 z,在请求的时候,我可以通过 header 告知,下载第 2 个位置到第 4 个位置。下载完成后,内容就是: bcd, 所以下载的是新构建内容的部分。使用的响应请求头是 multipart/byteranges
  • 合并内容,得到了下载的部分内容后,应该要本地再组装才能还原成一个完整的安装文件
  • 源码中,我们可以看到 AppImageUpdater.tsNsisUpdater.ts 会使用到, 会使用到差异下载, electron-builder/packages/electron-updater/src/differentialDownloader at master · electron-userland/electron-builder

如何禁用

这个流程还是挺麻烦的, 并且不支持 Mac, 打包构建它的时候,还挺花时间的。

Remove Block Map · Issue #2900 · electron-userland/electron-builder

electron-builder.yml 文件的定义:electron-builder/packages/app-builder-lib/src/configuration.ts at master · electron-userland/electron-builder

mac 的 zip 需要 patch 代码

源码没有对应的配置项。需要需要关闭的话, 修改electron-builder/packages/app-builder-lib/src/targets/ArchiveTarget.ts at master · electron-userland/electron-builder ,注释掉:

 updateInfo = await createBlockmap(artifactPath, this, packager, artifactName)