【译】Electron 自动更新的完整教程(Windows 和 OSX)

原文链接: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 动画


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

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

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

坏消息是(至少对于 Mac 用户):我不能在 OSX 上正确地生成安装包,所以我建议你下载一个 Windows 虚拟机(如 VirtualBox、parallels),并安装 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 的性能优化》。有兴趣的同学可点击查阅哦。

你可能感兴趣的:(自动更新,electron)