从源码解析Electron的安装为什么这么慢

前言

Electron作为一款跨平台的桌面应用端解决方案已经风靡全球。作为开发者,我们几乎不用关心与操作系统的交互,直接通过Web前端技术与Electron提供的API就可以完成桌面应用端的开发。

然而,为什么国内使用Electron的踩坑文章数不胜数,主要原因是Electron为了支持跨平台,为不同的操作系统平台进行了适配,将chromium内核与node集成到了一起,屏蔽了底层操作系统的细节,所以在不同的平台上有着不同的二进制基座。在开发的过程中,我们必须要下载对应的平台的基座,才能正常开发。也就是说,我们npm install electron -D的时候,一定是下载了Electron的二进制基座的。那么这个下载的过程在哪里?为什么速度这么慢呢?本文将通过Electron的安装源码一一说明。

安装Electron

在安装之前,我们先模拟一下没有配置任何关于Electron二进制镜像的npm配置文件,在~/.npmrc里面,只有一些默认的配置:

1
2
3
4
5
# ~/.npmrc文件
registry=https://registry.npm.taobao.org/
prefix=D:\Programs\nodejs\global_modules
cache=D:\Programs\nodejs\cache_modules
python=D:\Programs\Python39\python.exe

然后,创建一个名为electron-install-example的文件夹作为本此测试的Demo项目目录,并在进入该目录后执行npm init初始化node项目。

最后,使用命令行安装Electron:npm install electron -D。在短暂的npm包安装后,我们会发现会卡在一个地方:

这时候,很多开发者就会开始在网络上搜索:‘安装Electron卡住’,并且也很容易得到解决方案:

~/.npmrc文件中,单独设置Electron的镜像electron_mirror="https://npm.taobao.org/mirrors/electron/"

于是我们按照搜来的解决方案重新配置我们的.npmrc文件:

1
2
3
4
5
6
7
# ~/.npmrc文件
registry=https://registry.npm.taobao.org/
prefix=D:\Programs\nodejs\global_modules
cache=D:\Programs\nodejs\cache_modules
python=D:\Programs\Python39\python.exe
# 单独设置Electron的镜像
electron_mirror="https://npm.taobao.org/mirrors/electron/"

设置完成后,重新进行npm install,发现能够很快完成下载并继续开发。通过本文,我们深入细节,看看为什么Electron设置了单独的镜像后,就能够正常且快速完成下载安装。

深入下载细节

进入项目根目录下/node_modules/electron/(后续除特殊情况外,提到的目录路径都是统一相对于项目根目录)目录中,查看package.json文件中的scripts脚本节点:

了解npm的朋友们知道,postinstall中的脚本会在npm包完成安装后执行。

也就是说,npm install -D electron完成以后,会在node_modules/electron目录中立刻执行node install.js。所以,我们进一步查看install.js文件,看看它到底执行了什么。核心代码如下:

代码特别容易理解:在没有缓存文件的时候,会使用@electron/get提供的downloadArtifact函数,进行Electron二进制制品的下载。

于是,我们又将目标转移到@electron/get。这是个什么东西呢?查询官方仓库:官方仓库,就能够大概知道该工具的功能了:提供一定的参数来向远端下载文件。

找到@electron/get的模块入口node_modules/@electron/get/dist/cjs/index.js,也很容易从中找到downloadArtifact的函数定义:

该函数的文档:下载Electron发行制品,并且返回下载后的制品的绝对路径。而函数内部主要流程如下:

  • 解析要下载的制品对应的操作系统和平台。例如是Windows还是Linux,架构是x86还是AMD64。

  • 解析要下载的制品的版本。

  • 解析要下载的制品的具体文件名。例如要下载Windows下的64位的Electron制品,那么默认文件名称是:electron-v11.0.2-win32-x64.zip

  • 解析要下载的制品所在的远端URL是多少(与本文相关的重点)。

  • 处理本地缓存。

本文主要解析下载以及从本地缓存制品两个环节。

远端下载的URL

从上面的源码图中,我们会看到远端的URL来自于artifact_utils_1.getArtifactRemoteURL(artifactDetails)这个的返回,而该函数在@electron/get/dist/cjs/artifact-utils.js中进行定义:

该函数的定义也不难,主要流程如下:解析得到base变量,解析得到path变量,解析得到file变量,组合为${base}${path}/${file}。当然,你也可以在mirrorOptions中定义resolveAssetURL函数来返回自定义的地址。

在上面的处理流程中,能够看到一个频繁出现的函数:mirroVar。该函数也在该文件中定义,其函数定义如下:

1
2
3
4
5
6
7
8
9
10
function mirrorVar(name, options, defaultValue) {
// Convert camelCase to camel_case for env var reading
const lowerName = name.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}_${b}`).toLowerCase();
return (process.env[`NPM_CONFIG_ELECTRON_${lowerName.toUpperCase()}`] ||
process.env[`npm_config_electron_${lowerName}`] ||
process.env[`npm_package_config_electron_${lowerName}`] ||
process.env[`ELECTRON_${lowerName.toUpperCase()}`] ||
options[name] ||
defaultValue);
}

该函数主要返回参数name相关的变量值,以name = 'mirror'为例,获取过程为:

customDir进行下划线分割转换,得到const lowerName = 'mirror'(name为’customDir’则转换为’custom_dir’)。

依次检查如下环境变量值:

  1. NPM_CONFIG_ELECTRON_MIRROR
  2. npm_config_electron_mirror
  3. npm_package_config_electron_mirror
  4. ELECTRON_MIRROR
  5. options[‘mirror’]

上述任意变量存在值则直接使用,否则,使用默认值defaultValue

读到这里,也许有读者疑惑了,我明明是在.npmrc文件中配置的ELECTRON_MIRROR变量,而这里读取的明明是环境变量里面的值,怎么会有呢?如果直接使用node作为入口,那么确实不会有这些变量,但是通过npm运行就不一样了。这里用一个小例子来说明。

首先在一个node项目中编写一个脚本env-test.js:

1
console.log(process.env);

我们通过使用node运行该js脚本:

1
node env-test.js

看到命令行的输出,只会有当前机器的环境变量:

但是一旦通过npm进行运行,又会不一样:

运行命令npm run dev,会得到如下的结果,这里本人使用IDEA的断掉调试,会更加清晰的看到env的值:

通过npm run的方式,我们发现我们在~/.npmrc文件中配置的一些参数,都能在这里得到,并且是以npm_config_作为开头的。可能还有读者有疑惑,上面读取的变量,都是同意大小写的,这里是npm_config_ELECTRON_MIRROR,能读取到吗?事实上,env的读取是忽略大小写的:

综合目前的研究,相信读者已经清楚了为什么通过配置ELECTRON_MIRROR在.npmrc能够达到加快Electron二进制基座的下载速度的目的了,至于一些其他的配置变量,可以阅读附录的官方文档翻译。

本地缓存机制

有的读者看了上述的远端下载可能会说,我的机器就在内网环境,内网也没有镜像让我来写,我该怎么下载呢?实际上,@electron/get也不会完全从远端下载制品。它在下载的过程,会优先进行本地缓存文件的查找,如果已经存在了缓存好的制品,自然也就不会从远端下载了。那么这个查找缓存的过程是怎样的呢?或者说,@electron/get会从本地哪个目录去查找呢?让我们回到@electron/get/dist/cjs/index.js脚本的downloadArtifact函数中,看该部分:

url变量获取的下一行,构建了一个Cache缓存对象,继续往下,通过判断不进行强制从远端下载的标志,会进入getPathForFileInCache函数返回一个本地的缓存文件路径,如果路径不为空则使用它。所以,我们只需要让这个函数能够返回一个合法的缓存文件路径就能让@electron/get不进行远端下载,而是使用本地的缓存文件。所以我们跟到该函数中:

函数最终会使用上一节中的url变量形成一个本地的缓存路径,至于代码中的url.format以及filenamify的效果,读者可以自行编写Demo验证。

最后,路径还使用到了this.cacheRoot,查看Cache的构造函数,发现如果没有传递cacheRoot,则使用defaultCacheRoot,该值在该脚本文件上面有定义:

通过一段脚本输出该路径:

1
2
3
4
5
6
7
8
9
const env_paths_1 = require("env-paths");

const defaultCacheRoot = env_paths_1.default('electron', {
suffix: '',
}).cache;

console.log(defaultCacheRoot);
// 在本人的机器上输出:
// C:\Users\w4ngzhen\AppData\Local\electron\Cache

所以在Windows机器下,默认的缓存目录在~/AppData/Local/electron/Cache/,在本人的机器上,已经缓存的文件如下:

源码个人认为也不用继续解析了,读者结合文件夹名称应该能够很容易分析。

附录:@electron/get 官方Wiki翻译

下载Electron发行版制品

使用

基础方式:下载一个Electron二进制ZIP

1
2
3
4
5
import { download } from '@electron/get';

// NB: Use this syntax within an async function, Node does not have support for
// top-level await as of Node 12.
const zipFilePath = await download('4.0.4');

进阶:下载macOS下带有调试符号的Electron文件

1
2
3
4
5
6
7
8
9
10
11
import { downloadArtifact } from '@electron/get';

// NB: Use this syntax within an async function, Node does not have support for
// top-level await as of Node 12.
const zipFilePath = await downloadArtifact({
version: '4.0.4',
platform: 'darwin',
artifactName: 'electron',
artifactSuffix: 'symbols',
arch: 'x64',
});

指定镜像

下列选项可以用来指定从其他的地方下载Electron资源:

  • mirrorOptions Object(JavaScript对象)
    • mirror String (可选) - 下载资源的镜像地址的基础URL。
    • nightlyMirror String (可选) - Electron nightly-specific版本的镜像URL。
    • customDir String (可选) - 下载资源的目录名称,通常由版本号来设定。
    • customFilename String (可选) - 将要下载的资源的文件名称。
    • resolveAssetURL Function (可选) - 允许通过编程方式来进行资源下载的函数回调。

下载资源的URL进行如下的分解,每一项都来可以映射到mirrorOptions:

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { download } from '@electron/get';

const zipFilePath = await download('4.0.4', {
mirrorOptions: {
mirror: 'https://mirror.example.com/electron/',
customDir: 'custom',
customFilename: 'unofficial-electron-linux.zip'
}
});
// 上述将会从如下URL下载:
// https://mirror.example.com/electron/custom/unofficial-electron-linux.zip

const nightlyZipFilePath = await download('8.0.0-nightly.20190901', {
mirrorOptions: {
nightlyMirror: 'https://nightly.example.com/',
customDir: 'nightlies',
customFilename: 'nightly-linux.zip'
}
});
// 上述将会从如下URL下载:
// https://nightly.example.com/nightlies/nightly-linux.zip

customDir参数可以使用{{ version }}占位符来设置版本(务必注意:{}括号之间一定要有空格,否则会解析失败,即,{{[空格]version{空格}}}),这个占位符将会由所下载的资源的版本(没有首字符v)来动态替换。例如:

1
2
3
4
5
6
7
8
9
10
const zipFilePath = await download('4.0.4', {
mirrorOptions: {
mirror: 'https://mirror.example.com/electron/',
customDir: 'version-{{ version }}',
platform: 'linux',
arch: 'x64'
}
});
// 将会从如下的URL下载:
// https://mirror.example.com/electron/version-4.0.4/electron-v4.0.4-linux-x64.zip

使用环境变量来指定镜像选项

镜像配置选项也可以通过如下的环境变量来指定:

  • ELECTRON_CUSTOM_DIR - 指定资源下载的自定义目录。
  • ELECTRON_CUSTOM_FILENAME - 指定资源下载的自定义文件名。
  • ELECTRON_MIRROR - 指定如果版本没有使用nightly的时候,服务器的下载URL。
  • ELECTRON_NIGHTLY_MIRROR - 指定如果版本使用nightly的时候,服务器的下载URL。

重写下载的资源版本

所下载的资源的版本可以通过设置``ELECTRON_CUSTOM_VERSION 环境变量来进行覆盖。设置该版本将会覆盖传入download或是downloadArtifact`函数的version参数。

它是如何运行的

下载Electron资源到操作系统中已知的位置,并且缓存该资源的模块,用于便于在将来请求同一个资源的时候能够立刻完成并返回。缓存路径如下:

  • Linux: $XDG_CACHE_HOME or ~/.cache/electron/
  • MacOS: ~/Library/Caches/electron/
  • Windows: %LOCALAPPDATA%/electron/Cache or ~/AppData/Local/electron/Cache/

默认情况下,该模块使用 got作为下载器。因此,您可以通过downloadOptions使用与get相同的选项(options)来进行下载。

进度条

默认情况下,下载工件超过30秒时会显示进度条。若要禁用,请将ELECTRON_GET_NO_PROGRESS 环境变量设置为任何非空值,或设置downloadOptions中的quiettrue。如果您需要通过API自己监视进度,请设置downloadOptions中的getProgressCallback 回调,其函数签名与gotdownloadProgress event callback相同。

代理

下游软件包应利用 initializeProxy功能来添加HTTP(S)代理支持。如果设置了环境变量ELECTRON_GET_USE_PROXY,则会自动调用它。根据使用的Node版本,使用不同的代理模块.因此,设置代理环境变量的方式略有不同。对于Node 10及更高版本,使用global-agent。否则,将使用global-tunnel-ng。请参阅相应的链接模块以确定如何配置代理支持。