34

原文链接:Auto-updating apps for Windows and OSX using Electron: The complete guide

2017.11.06 更新:electron-builder 提供了 electron-updater 模块,具体请查阅:《Quick and painless automatic updates in Electron》

由于我之前也调研了 Electron 的自动更新方面的知识,所以我会在保留原文所有信息的前提下,加入了一些备注(如作者的一些错误信息和补充了我个人的一些认识)。


通过 Electron,你可能只需一眨眼的时间就完成了一个不错的桌面应用,并分发到用户手中。当你觉得自己能像一个侥幸的坏蛋一样轻松时,你可能会意识到你遗漏了一个重要的点:用户如何获取下一个版本呢?甚至该新版本新增了一些优秀的功能。当然,他们能删除后再重新安装该应用,但这难道不蹩脚吗?

快速浏览 Electron 文档 时,你会注意到该文档中含有 auto-updater 模块,它仅仅是另一个框架——Squirrel 的接口。Squirrel 会在背后检测(或你主动触发)是否有新版本、下载新版本,并在你启动或重启应用时自动更新应用。

但悲伤的是:实际实现起来并不是文档上写的这么简单。因为自动更新在 OSX 和 Windows 上的工作方式并不相同(目前并不支持 Linux),并且这两者的文档是分散在多个库(repository)中。我已经花费了大量的时间把该功能实现了。所以我觉得将我所学习到的知识总结成一篇教程是值得的,希望它能节省你的时间。

虽然这里所讲的一切应该均能在 Windows 和 OSX 上运行,但为了减少异议,我先声明我是在 Mac OSX 10.11 上执行的操作,除了为 Windows 系统构建安装包(在虚拟机上)。

如对该篇教程有任何改善或更新的建议,可在 twitter 联系我!

应用打包

在实现自动更新之前,有一个重要的步骤 —— 打包。我假设大多数人已经知道如何通过 electron-packager 实现该操作,但有两件事是时常被忽略的。

{
  "name": "MyApp",
  "main": "app.js",
  "private": true,
  "productName": "MyApp",
  "version": "1.0.0",
  "author": "My Company Ltd",
  "description": "MyApp",
  "devDependencies": {
    "electron-installer-squirrel-windows": "^1.3.0",
    "electron-packager": "^5.1.1",
    "electron-prebuilt": "0.36.7"
  },
  "scripts": {
    "start": "NODE_ENV=development ./node_modules/.bin/electron .",
    "pack:osx": "./node_modules/.bin/electron-packager . $npm_package_productName --app-version=$npm_package_version --version=0.36.7 --out=builds --ignore='^/builds$' --platform=darwin --arch=x64 --sign='Developer ID Application: My Company Ltd (ABCDEFGH10)' --icon=icon.icns --overwrite",
    "pack:win": "./node_modules/.bin/electron-packager . $npm_package_productName --app-version=$npm_package_version --version=0.36.7 --out=builds --ignore='^/builds$' --platform=win32 --arch=ia32 --version-string.CompanyName='My Company Ltd' --version-string.LegalCopyright='Copyright (C) 2016 My Company Ltd' --version-string.FileDescription=$npm_package_productName --version-string.OriginalFilename='MyApp.exe' --version-string.InternalName=$npm_package_productName --version-string.ProductName=$npm_package_productName --version-string.ProductVersion=$npm_package_version --asar=true --icon=logo.ico --overwrite"
  }
}

package.json

注意 package.json 的额外字段 —— productNameauthordescription,虽然这几个字段并不是打包必备的,但它们会在 Windows 的 Squirrel 安装包中使用到。

为应用执行代码签名(Code-signing)的这部操作并不是自动更新的必备步骤(译者注:也许作者当时的 Electron 版本的自动更新模块不必进行代码签名,但当前版本是必须要进行这部操作的,官方文档中写道:Your application must be signed for automatic updates on macOS. This is a requirement of Squirrel.Mac. ),但这是非常可取的操作。对于 OSX,你需要一个 Apple 的开发者认证,然后在 script 字段的 pack:osx 替换以下参数即可:

--sign='Developer ID Application: My Company Ltd (ABCDEFGH10)'

在 OSX 中,你可以通过 Keychain Access > My Certificates 查看(应用程序 -> 钥匙串 > 我的证书,如果有的话)。

我并没有在 Windows 上执行代码签名这项操作,但你可以看看该主题相关的优秀教程。

对于 Windows,推荐为 electron-packager 传递 version-string 的所有可选参数,如 company name、product name 等。因为一旦我们生成 Windows 的 Squirrel 安装包,该应用就能在 Windows 的『开始』菜单显示正确的元信息(metadata),而不是 Atom 的默认信息。

Atom Shell is now called Electron。

所以,让我们开始吧!

OSX

在 OSX 中,自动更新是通过 Squirrel.Mac 处理的,它是内置于 Electron 中。这意味着你只需打包你的应用,然后照常运行就好!

恩,其实不完全是。

Squirrel.Mac 的工作方式是通过访问一个你所提供的 API 『路径』(endpoint),判断是否有新版本。如果没有新版本,那么该路径应该返回 HTTP 204。如果有新版本,则它会期待接收一个 HTTP 200、且是 JSON 格式 的响应,其中包含一个 能获取 .zip 文件的 url

PS:『路径』又称"终点"(endpoint),表示API的具体网址。

{
  "url": "http://mysite.com/path/to/zip/MyApp.zip"
}

在得到该 url 后,Squirrel 会构造一个 application/zip 的请求去访问该 url,下载相应文件,然后触发最终事件(下载完成)让你知道更新包即将安装。对于你来说,所有事情的处理都是自动化的。

如果你不十分确定服务器程序应该长什么样,可看看下面的一个超级小型的 Node.js/Express 服务,假定它的目录结构如下:

└── releases
 ├── darwin
 │ ├── 1.0.0
 │ ├── 1.0.2
 │ └── 1.0.3
 └── win32
{
  "name": "squirrel-version-checker",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "PORT=80 node app.js",
    "dev": "./node_modules/.bin/nodemon app.js"
  },
  "dependencies": {
    "express": "^4.9.8",
    "morgan": "^1.3.2"
  },
  "devDependencies": {
    "nodemon": "^1.8.1"
  }
}

基于 Node 的更新服务 package.json

'use strict';
const fs = require('fs');
const express = require('express');
const path = require('path');
const app = express();

app.use(require('morgan')('dev'));

app.use('/updates/releases', express.static(path.join(__dirname, 'releases')));

app.get('/updates/latest', (req, res) => {
  const latest = getLatestRelease();
  const clientVersion = req.query.v;

  if (clientVersion === latest) {
    res.status(204).end();
  } else {
    res.json({
      url: `${getBaseUrl()}/releases/darwin/${latest}/MyApp.zip`
    });
  }
});

let getLatestRelease = () => {
  const dir = `${__dirname}/releases/darwin`;

  const versionsDesc = fs.readdirSync(dir).filter((file) => {
    const filePath = path.join(dir, file);
    return fs.statSync(filePath).isDirectory();
  }).reverse();

  return versionsDesc[0];
}

let getBaseUrl = () => {
  if (process.env.NODE_ENV === 'development') {
    return 'http://localhost:3000';
  } else {
    return 'http://download.mydomain.com'
  }
}

app.listen(process.env.PORT, () => {
  console.log(`Express server listening on port ${process.env.PORT}`);
});

一个简单地、用于测试 Squirrel.Mac 自动更新的 Express 服务器

这将会从本地的文件系统进行分发文件,但这不是理想的处理方式。我的建议是:将这些文件放置在 Amazon S3。

Amazon S3:Amazon Simple Storage Service

然后你可以在开发环境下,通过 Electron 访问该路径:

http://localhost:3000/updates/latest?v=1.0.1

?v=1.0.1 是你当前应用的版本。

现在你已经拥有了服务器程序和路径了,那么在应用中处理更新操作就十分简单了。

在 Electron 的主进程文件中,引入 auto-updater 模块,然后获取当前系统和应用的版本:

const autoUpdater = require('auto-updater');
const appVersion = require('./package.json').version;
const os = require('os').platform();

然后配置路径,该路径会因系统(Windows 和 Mac)不同而有所差异(至于原因,会在 Windows 章节看到):

var updateFeed = 'http://localhost:3000/updates/latest';

if (process.env.NODE_ENV !== 'development') {
  updateFeed = os === 'darwin' ?
    'https://mysite.com/updates/latest' :
    'http://download.mysite.com/releases/win32';
}

autoUpdater.setFeedURL(updateFeed + '?v=' + appVersion);

告诉 Electron 到哪里检测新版本

autoUpdater 模块提供了一些事件,你可通过渲染进程触发它们(译者注:通过 IPC 通讯模块),想获取更多信息,可查阅 auto-Updater 文档页面 。相关交互的实现决定取决于你如何处理这些事件(如发生错误等),并通知用户。但你最后一步应该做的是:

autoUpdater.quitAndInstall();

将上述语句放在主进程文件后,应用会以新本版的形式重启。赞!

Windows

如你想象的那样,在 Windows 上实现自动更新是通过 Squirrel.Windows。但它的处理方式与 OSX 完全不同。

与 Squirrel.Mac 不同的点在于:Squirrel.Windows 并不需要一个用于检测新版本的 API 路径,它需要的是一个文件服务器,所以你可以简单地将文件拖拽到 Amazon S3 bucket 上。另外,该 Squirrel 更新器并不内置于 Electron,它是一个第三方依赖。这意味着你需要为你所打包的 Windows 应用生成一个安装器,这样它才会包含 Squirrel 更新器。

Amazon S3 bucket:S3 的数据存储结构非常简单,就是一个扁平化的两层结构:一层是存储桶(Bucket,又称存储段),另一层是存储对象(Object,又称数据元)。具体信息可查看 《亚马逊S3服务介绍》

好消息是:Windows 的安装包和更新器的运行过程顺滑的。因为当你启动 Setup.exe 时,你会发现安装和启动该应用是迅速的。没有无聊的安装向导和一直按“下一步”、最后按“完成”的步骤,不然与大多数 Windows 安装器如出一辙。当然,它也能生成 delta packages,这让你在执行更新时,不必下载整个应用,这真的是一流啊。

译者注:我通过 electron-builder 生成的 Windows 安装包与我们常见的软件安装界面不太一样,他没有安装向导和点击“下一步”,只有一个安装时的 gif 动画(默认的 gif 动画如下图),因此也就没有让用户选择安装路径等权利。也许作者习惯了 Mac 的安装方式(即下面第二幅图),所以会觉得 Windows 的安装包比较繁琐。

Windows 安装时默认的 gif 动画
Windows 安装时 默认显示的 gif 动画

Mac 常见的安装模式
Mac 常见的安装模式,将“左侧的应用图标”拖拽到“右侧的 Applications”即可

如果你想为 Windows 应用生成常见的、需要点击“下一步”的(即用户可自定义的)安装包,可以通过 NSIS 程序,具体可看这篇教程《[教學]只要10分鐘學會使用 NSIS 包裝您的桌面軟體–安裝程式打包。完全免費。》。当然,前提还是通过 electron-packager 打包程序。

NSIS(Nullsoft Scriptable Install System)是一个开源的 Windows 系统下安装程序制作程序。它提供了安装、卸载、系统设置、文件解压缩等功能。这如其名字所指出的那样,NSIS 是通过它的脚本语言来描述安装程序的行为和逻辑的。NSIS 的脚本语言和通常的编程语言有类似的结构和语法,但它是为安装程序这类应用所设计的。

坏消息是(至少对于 Mac 用户):我不能在 OSX 上正确地生成安装包,所以我建议你下载一个 Windows 虚拟机(如 VirtualBoxparallels),并安装 Node.js。

译者注:我通过 electron-builder,可在 MacOS 中直接(即不通过虚拟机)生成 Windows 安装包(即Setup.exe)。具体可 查看这里

假设你已经配置好并设置了正确的更新源,那么在上述 OSX 章节的代码基础上,还需要处理一些 Squirrel.Windows 事件,这些事件与 OSX 上的不同。你可以查看该 案例。然而,这里提供一个更简单的方式,仅需安装 electron-squirrel-startup npm 模块:

npm install electron-squirrel-startup --save-dev

然后在 Electron 的主进程文件顶部添加以下一行语句:

if (require('electron-squirrel-startup')) return;

Squirrel.Windows 事件应该被尽早处理,显然,这是要走的路。

最后,为了生成安装包,我们会使用 Atom 的 grunt-electron-installer。为什么它是一个 grunt 插件,而不是一个简单的命令行工具——我不知道,但它就是解决方法。

更新:Electron 团队开发了一个独立的安装器打包工具——electron-winstaller,它拥有与 grunt task 同样的 API

将 Electron-packager 生成的 win32 文件夹打包压缩(zip),然后将其复制到虚拟机上。在该文件夹外(译者注:在解压后),你需要配置 grunt task,该 task 会生成安装包,因此你应该首先安装所有依赖:

npm install -g grunt-cli
npm install grunt grunt-electron-installer --save-dev

假设 Windows 编译后的包放置在一个称为 MyApp-win32-ia32 的文件夹下。下面展示 Gruntfile 的样子:

module.exports = function(grunt) {
  grunt.initConfig({
    'create-windows-installer': {
      ia32: {
        appDirectory: './MyApp-win32-ia32',
        outputDirectory: './dist',
        name: 'MyApp',
        description: 'MyApp',
        authors: 'My Company Ltd',
        exe: 'MyApp.exe'
      }
    }
  });

  grunt.loadNpmTasks('grunt-electron-installer');
};

需要注意的是:如果你想为你的文件和安装包进行代码签名(code-sign)操作,你也需要为该 task 配置提供所有参数。

运行该 grunt task 后,会在 ./dist 目录下产生一堆文件:

grunt create-windows-installer

你预期看到的与下面类似:

└── dist
 ├── MyApp.1.0.0.nupkg
 ├── MyApp-1.0.0-full.nupkg
 ├── RELEASES
 ├── Setup.exe

在下一次发布时,该安装器也会自动生成一个 delta packages。

现在进行最简单的一步 —— 拖拽这些文件到 S3 bucket 进行上传。然后 url 指向该文件夹(包含 RELEASESnupkg 文件)。当应用运行在 Windows 系统上时,它会将该 url 设置到 updateFeed 参数上(因为我们在先前的 OSX 章节处已实现)。

注意:目前有一个与安装器的 node-rcedit 模块相关的问题,该模块会在你尝试去修改 .exe 文件的一些元信息和替换默认图标(icon)时抛出错误。你可以在 这里查看该 issue。因此,目前如果你想为安装器文件修改 icon 或为其赋予实际数据,你可能不得不手动地通过 ResHacker 进行修改。

结束语

希望这篇文章能作为一个好的起点,能帮助和服务每一个正在为 Electron 应用实现自动更新的朋友们。如果你发现任何我遗漏的点,或有任何改善的建议,欢迎在 twitter 告诉我!另外,请记住 Electron 是一个快速发展的框架,所以要确保你阅读的是你当前版本的文档。Electron 的 API 也是更新十分频繁的。


另外,凹凸实验室基于 Electron 和 Vue 开发了一个 Excel 数据清洗工具 XCEL,并根据此项目总结出了一篇博文《XCel 项目总结 - Electron 与 Vue 的性能优化》。有兴趣的同学可点击查阅哦。


思考中
1.9k 声望100 粉丝