Skip to content

发布

一个程序在开发完成之后,就需要被打包成指定格式的文件并分发给最终用户。
Electron为开发者提供了多种功能完善的打包发布工具,比如electron-packager和electron-builder等。
它们都有丰富的配置项和API,开发者可以使用它们完成不同需求的打包任务,而且这些打包工具在各个平台上的打包方式是基本一致的。

生成图标

在编译打包应用程序之前,你要先为应用程序准备一个图标。
图标建议为 1024*1024 尺寸的 png 格式的图片。把图标文件放置在 [your_project_path]/public 目录下,然后安装 electron-icon-builder 组件:

1
> yarn add electron-icon-builder --dev

接着在package.json的scripts配置节增加如下指令的配置:

1
"build-icon": "electron-icon-builder --input=./public/icon.png --output=build --flatten"

然后执行生成应用程序图标的指令:

1
> yarn build-icon

此时会在 [your_project_path]/build/icons 中生成各种大小的图标文件,如图13-1所示。

图13-1生成的应用程序图标

图13-1 生成的应用程序图标

生成的这些图标将被打包编译进最终的可执行文件内。这个工具可以帮助团队美工节省大量的重复工作的时间。

生成安装包

Electron生态下有两个常用的打包工具:electron-packager(https://github.com/electron/electron-packager)、electron-builder(https://www.electron.build/)。下面我们来介绍一下它们的不同:

electron-packager依赖于Electron框架内部提供的自动升级机制,需要自己搭建自动升级服务器,才能完成自动升级工作。
electron-builder则内置自动升级机制,把打包出的文件随意存储到一个Web服务器上即可完成自动升级(比如七牛云对象存储服务或阿里云的OSS服务)。

这两个项目都是Electron官方团队维护的,electron-packager直接挂载Electron组织下,electron-builder项目挂在electron-userland组织(也由Electron团队创建)下。

基于以上对比,显然electron-builder更胜一筹。
接下来我们就主要介绍如何使用electron-builder来打包我们开发的Electron应用。

如果你是使用Vue CLI Plugin Electron Builder创建的项目,那么工程内自带electron-builder。
你只需要执行package.json中默认配置的打包指令即可完成打包工作:

1
> yarn electron:build

打包过程比较慢,需耐心等待。打包完成后会在 [your project]/dist_electron 目录下生成一系列的文件,如图13-2所示。

图13-2打包好的安装文件

图13-2 打包好的安装文件

其中 [your_project_name]Setup[your_project_version].exe 即你要分发给你的用户的安装文件。
Mac环境下待分发的文件为 [your_project_name]-[your_project_version].dmg

[your_project_path]/dist_electron/win-unpacked 这个目录下存放的是未打包的可执行程序及其相关依赖库。
Mac环境下的.app文件存放在 [your_project_path]/dist_electron/mac 目录下。

扩展:  
除electron-packager和electron-builder外,你还可以考虑使用 electron-forge(https://www.electronforge.io/)作为你的打包工具。
这个项目也是由Electron团队开发的,目标是使Electron项目的创建、发布、安装工作更简便。
以此工具创建的项目可以很方便地集成webpack和原生的Node.js模块。
它也可以把应用程序打包成安装文件,但需要自己搭建服务器才能实现自动升级,不像electron-builder那样简单。

除此之外,还有专门为Windows平台打包的工具electron-installer-windows(https://github.com/electron-userland/electron-installer-windows)和专门给Linux平台打包的electron-installer-snap(https://github.com/electron-userland/electron-installer-snap),这两个工具也都是由Electron团队开发的。

扩展:   前文我们提到,Electron的缺点之一就是安装包巨大,如不做特殊处理,每次升级就都相当于重新下载了一次安装包,虽然现在网络条件越来越好,但动辄四五十兆大小的安装包还是给应用的分发带来了阻碍。
一个可行的解决方案是:使用C++开发一个应用程序安装器,开发者把这个安装器分发给用户,当用户打开这个安装器的时候,安装器下载Electron的可执行文件、动态库以及应用程序的各种资源,下载完成后创建开始菜单图标或桌面图标。
当用户点击应用图标启动应用时,启动的是Electron的可执行程序。
由于安装器逻辑简单,没有携带与Electron有关的资源,所以可以确保安装器足够轻量,易于分发。

当应用程序升级的时候,如果不需要升级Electron自身,就不必下载Electron的可执行程序及动态库,只需要下载你修改过的程序文件及资源即可(可能仅仅是一个asar文件)。

如果你没有一个高性能服务器,那么用户通过安装器下载应用所需文件时,可能会对你的服务器造成巨大的网络压力,这时你可以考虑把这些文件放置在CDN服务器上,或者让安装器分别从两处下载:从淘宝镜像网站下载Electron的可执行文件及其动态库,从你的服务器下载应用程序文件及其资源文件。

如果你的应用只会分发给Windows用户,那么你可以通过配置electron-builder使其内部的NSIS打包工具从网络下载程序必备的文件,这样就不用再开发一个独立的安装器了,具体配置文档详见 https://www.electron.build/configuration/nsis#web-installer

代码签名

在应用程序分发给用户的过程中,开发者很难知道应用是否被篡改过。如果不加防范,很可能会对用户造成伤害。
操作系统也提供了相应的机制来提醒用户这类风险:在Mac操作系统下,默认只允许安装来自Mac App Store的应用;
Windows系统下安装未签名的应用程序会给出安全提示。作为一种安全技术,代码签名可以用来证明应用是由你创建的。

如果要给Windows应用程序签名,首先我们需要购买代码签名证书。
你可以在digicert(https://www.digicert.com/code-signing/microsoft-authenticode.htm)、Comodo(https://www.comodo.com/landing/ssl-certificate/authenticode-signature/)或GoDaddy(https://au.godaddy.com/web-security/code-signing-certificate)等平台按时间付费的方式购买代码签名证书。

扩展 
个人开发者可能会期望寻求免费的代码签名证书,对此我做过简单的调研。然而即使是类似Let's Encrypt(https://letsencrypt.org)这种全球知名的免费证书颁发机构也不做代码签名证书。主要原因是他们无法验证现实世界申请人的身份是否真实、合法。
他们可以做到由机器自动颁发和确认HTTPS证书,但给软件颁发证书这个过程从安全角度而言很难做到全自动。

在实际使用中,如果能说服用户信任你分发的软件,那么即使不为软件做代码签名也没什么大碍。

如果你使用electron-builder来打包你的应用,待证书购买好之后可以按照 https://www.electron.build/configuration/win 文档所述步骤进行证书的配置。

如果你是使用Vue CLI Plugin Electron Builder创建的项目,配置信息将被放置在vue.config.js中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module.exports = {
    productionSourceMap: false,
    pluginOptions: {
        electronBuilder: {
            builderOptions: {
                win: {
                    signingHashAlgorithms: ['sha1', 'sha256'],
                    certificateFile: "./customSign.pfx",
                    certificatePassword:"********",
                    certificateSubjectName:'your certificate subject name'
                    // ...
                }
            },
            mainProcessFile: 'public/background/start.js',
        },
    }
}

其中customSign.pfx为证书颁发机构颁发给你的证书文件,certificatePassword为证书密码。

在Mac平台上使用electron-builder打包应用时,一般情况下不用做额外的配置,electron-builder会自动加载Mac系统下钥匙串中的证书。
但如果想获得证书则必须加入苹果开发者计划(这也是一个付费项目),关于如何加入苹果开发者计划并创建证书的详细步骤请参见本书附录A:Mac代码签名。

自动升级

如何让应用程序自动升级是桌面应用程序开发者需要考虑的一个重要问题。
开发者给应用增添了新功能、解决了老问题,如何把新版本的应用推送给用户呢?
有些用户已经升级了新版本,有些用户尚未升级新版本,怎么保证新旧版本的应用程序能同时运行呢?还有很多类似的问题都需要开发者关注
。Web开发人员则不用太担心这方面的问题,这也正是Web应用的优势之一。

electron-builder内置自动升级的机制。现在我们就模拟一下应用新版本发布的流程,看看electron-builder是如何完成自动升级的。

首先需要修改应用程序的配置文件,增加如下配置节:

1
2
3
4
publish: [{
    provider: "generic",
    url: "http://download.yoursite.com"
}]

如果你是使用Vue CLI Plugin Electron Builder创建的项目,此配置节应该在builderOptions配置节下(见上一节代码签名的配置样例)。
这里配置的URL路径就代表你把新版本安装包放在哪个地址下了,也就是说这是你的版本升级服务器。
这个服务器并没有特殊的要求,只要可以通过HTTP协议下载文件即可,所以阿里云的OSS服务或者七牛云对象存储服务均可使用。

接着需要在主进程中加入如下代码:

1
2
3
4
5
6
7
8
const { autoUpdater } = require('electron-updater');
autoUpdater.checkForUpdates();
autoUpdater.on('update-downloaded', () => {
    this.mainWin.webContents.send('updateDownLoaded');
})
ipcMain.on('quitAndInstall', (event) => {
    autoUpdater.quitAndInstall();
});

这段逻辑应在窗口启动后的适当的时机执行,尽量不要占用窗口启动的宝贵时间。

autoUpdater模块负责管理应用程序升级,checkForUpdates方法会检查配置文件中Web服务地址下是否存在比当前版本更新的安装程序,如果有则开始下载。
下载完成后将给渲染进程发送一个消息,由渲染进程提示用户“当前有新版本,是否需要升级”。
当用户选择升级之后,再由渲染进程发送'quitAndInstall'消息给主进程。
主进程接到此消息后,执行autoUpdater.quitAndInstall();方法,此时应用程序退出,并安装刚才下载好的新版本的应用程序。
安装完成后重启应用程序。

autoUpdater模块是根据package.json中的版本号来判断当前版本是否比服务器上的版本老的。
所以打包新版本时,一定要更新package.json中的version配置。

新版本打包完成后,Windows平台需要把 [your_project_name]Setup[your_project_version].exelatest.yml 两个文件上传到你的升级服务器;
Mac平台需要把 [your_project_name]-[your_project_version]-mac.zip[your_project_name]-[your_project_version].dmglatest-mac.yml 三个文件上传到你的升级服务器;
Linux平台需要把 [your_project_name]-[your_project_version].AppImagelatest-linux.yml 两个文件上传到你的升级服务器。

做好这些工作后,用户重启应用,将收到新版本通知。

如果你使用electron-packager或者electron-forge打包你的应用,需要自己搭建一个专有的升级服务器才能支撑应用的自动升级功能,这里我们就不多做介绍了。

踩坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ npm run electron:build


⨯ Get "http://npm.taobao.org/mirrors/electron/13.1.6/electron-v13.1.6-darwin-x64.zip": proxyconnect tcp: dial tcp :0: connect: can't assign requested address
github.com/develar/app-builder/pkg/download.(*Downloader).follow.func1
        /Volumes/data/Documents/app-builder/pkg/download/downloader.go:206
github.com/develar/app-builder/pkg/download.(*Downloader).follow
        /Volumes/data/Documents/app-builder/pkg/download/downloader.go:234
github.com/develar/app-builder/pkg/download.(*Downloader).DownloadNoRetry
        /Volumes/data/Documents/app-builder/pkg/download/downloader.go:128
github.com/develar/app-builder/pkg/download.(*Downloader).Download
        /Volumes/data/Documents/app-builder/pkg/download/downloader.go:112
github.com/develar/app-builder/pkg/electron.(*ElectronDownloader).doDownload
        /Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:192
github.com/develar/app-builder/pkg/electron.(*ElectronDownloader).Download
        /Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:177
github.com/develar/app-builder/pkg/electron.downloadElectron.func1.1
        /Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:73
github.com/develar/app-builder/pkg/util.MapAsyncConcurrency.func2
        /Volumes/data/Documents/app-builder/pkg/util/async.go:68
runtime.goexit
        /usr/local/Cellar/go/1.16/libexec/src/runtime/asm_amd64.s:1371  
ExecError: /Users/nocilantro/Desktop/various-clock/node_modules/app-builder-bin/mac/app-builder exited with code ERR_ELECTRON_BUILDER_CANNOT_EXECUTE
    at ChildProcess.<anonymous> (/Users/nocilantro/Desktop/various-clock/node_modules/builder-util/src/util.ts:249:14)
    at Object.onceWrapper (events.js:421:26)
    at ChildProcess.emit (events.js:314:20)
    at maybeClose (internal/child_process.js:1047:16)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:287:5) {
  exitCode: 1,
  alreadyLogged: false,
  code: 'ERR_ELECTRON_BUILDER_CANNOT_EXECUTE'
}

修改 .npmrc:

1
2
proxy=false
NO_PROXY=*