Electron安装过程深入解析(读完此文解决Electron安装失败导致的无法启动,无法打包的问题)
1. 安装Electron依赖包
开发者往往通过npm install(或 yarn add)指令完成为Node.js工程安装依赖包的工作,
安装Electron也不例外,下面是npm和yarn的安装Electron依赖包的指令:
npm install electron --save-dev
yarn add electron --dev
官方推荐我们把electron依赖包安装为开发依赖(devDependencies),
这实际上是为了将来制作应用程序安装包时,
避免把electron包和其可执行文件包装两次。这部分内容后文我们会详细讲解。
如果你希望观察npm install指令的具体执行细节,可以为其添加两个参数,如下所示:
npm install electron --save-dev --timing=true --loglevel=verbose
通过以上指令安装Electron依赖包,你会观察到整个安装过程的执行情况日志,
这里我截取几个重要的日志分析一下。
npm http fetch GET 200 https://registry.npm.taobao.org/electron 125ms
这是npm通过http协议获取electron包的注册信息的日志,
请求上述地址(我用的是npm淘宝源)将得到一个json响应,
json中包含了electron的所有版本的版本信息,
如果安装时我们没有为electron指定版本号,将安装最新的版本。
> electron@9.2.0 postinstall D:\ElectronDeepDive\capture1\install\node_modules\electron
> node install.js
这是Electron依赖包安装完成后,npm执行其依赖包内定义的postinstall钩子的日志。
npm包管理文档为npm包定义了一系列的钩子,postinstall钩子会在npm包安装完成后被执行,
除了postinstall钩子之外,常用的还有如下这些钩子:
- preinstall包安装之前执行;
- postuninstall包被卸载之后执行;
- preuninstall包被卸载之前执行;
- poststart当npm start执行后触发;
- poststop当npm stop执行后触发;
- posttest当npm test执行后触发;
详细的文档请参阅:https://docs.npmjs.com/misc/scripts
postinstall钩子定义在Electron包内的package.json中,代码如下:
"scripts": { "postinstall": "node install.js" }
install.js程序是Electron包内的一个重要程序,用于下载Electron的可执行文件及相关资源,下一小节我们将讲解Electron可执行文件的下载过程。
2. 下载Electron的二进制文件
在install.js中,程序获取了当前操作系统的版本,并通过如下代码下载Electron的二进制文件与相应的资源:
downloadArtifact({ version, artifactName: 'electron', force: process.env.force_no_cache === 'true', cacheRoot: process.env.electron_config_cache, platform: process.env.npm_config_platform || process.platform, arch: process.env.npm_config_arch || process.arch }).then(extractFile).catch(err => { console.error(err.stack) process.exit(1) })
downloadArtifact方法是@electron/get包提供的,这个包是Electron包依赖的一个npm包,
由于自npm 3.x以来,npm把包管理方式从嵌套结构切换到了扁平结构,所以@electron/get位于当前工程的node_modules目录的根目录下。
拓展:在npm 3.x以前,npm的包管理方式是嵌套结构的,
也就是说一个工程安装的依赖包位于当前工程根目录下的node_modules目录中,
假设其中一个依赖包又依赖了其他npm包,我们假设这个依赖包叫做packageA,
那么它的依赖包会被安装在packageA目录的node_modules目录下,
以此类推。以这种方式管理依赖包会导致目录层级很深,
在Windows操作系统中,文件路径最大长度为260个字符,目录层级过深会导致依赖包安装不成功。
并且不同层级的依赖中可能引用了同一个依赖包,
这种结构也没办法复用这个依赖包,而且这种情况非常常见,造成了大量的冗余、浪费。
自npm 3.x以来,npm的包管理方式升级为了扁平解构,
无论是当前工程的依赖包还是依赖包的依赖包,都会被优先安装到当前工程的node_modules目录下,
在安装过程中如果npm发现当前工程的node_modules目录下已经存在了相同版本的某个依赖包,
那么就会跳过安装过程,直接让工程使用这个已安装的依赖包,只有在版本不同的情况下,
才会在这个包的node_modules目录下安装新的依赖包。
这就很好的解决了前面两个问题。但也引来了新的问题,
直到npm 5.x引入了package lock的机制后,才解决了新的问题,这已超出了本书的讨论范围,
详情请参阅:https://docs.npmjs.com/configuring-npm/package-lock-json.html。
另外,npm判断两个依赖包是否版本相同,是有一套复杂的规则的,
这也超出了本书的讨论范围,详情请参阅:https://docs.npmjs.com/about-semantic-versioning。
downloadArtifact方法的入参是一个配置对象,
对象的force属性标记着是否需要强制下载Electron的二进制文件,
如果环境变量force_no_cache的值为"true"则无论本地有没有缓存,都会从Electron的服务器下载相应的文件。
配置对象的version属性是需要下载的Electron可执行程序的版本号,
这个版本号就是定义在Electron npm包的package.json内的版本号。
platform属性是当前的操作系统的名称,可能的值为"darwin"、"win32"或"linux"等,
arch是你当前操作系统的架构,可能的值为"x32"或"x64",
这些信息都是帮你确定下载什么版本的Electron可执行文件的。
上述信息最终被组装成的下载地址可能是如下的样子(其中版本号视真实情况而定):
https://github.com/electron/electron/releases/download/v9.2.0/electron-v9.2.0-win32-x64.zip
如果处于windows操作系统内,上述文件会被首先下载到如下目录中:
C:\Users\ADMINI~1\AppData\Local\Temp
这个目录是Node.js通过os.tmpdir()确定的。文件下载完成后,
程序会把它复制到缓存目录中以备下次使用,这个机制极大的节省了开发者的时间成本,
下一小节我们将深入讲解Electron安装过程中的缓存和镜像机制。
缓存完成后,上述代码中的extractFile回调方法被执行,
此方法会把缓存目录下的二进制文件压缩包解压到当前Electron依赖包的dist目录下:
[project]\node_modules\electron\dist
除了下载Electron二进制文件的压缩包外,downloadArtifact还单独下载了一个SHASUMS256.txt文件,
这个文件内记录了Electron二进制文件压缩包的sha256值,
程序会对比一下这个值与压缩包文件的sha256值是否匹配,
以避免用户请求被截获,下载到不安全的文件的情况(这方面的效用只能说聊胜于无),
或者是下载过程意外终止,文件数据不完整的情况。
3. 缓存与镜像策略
上文中我们提到Electron的二进制文件压缩包下载成功后,会复制一份到缓存目录,以备下次使用。在Windows环境下,默认的缓存目录为:
C:\Users\Administrator\AppData\Local\electron\Cache
这是通过Node.js的os.homedir()再附加了几个子目录确定的。
你可以通过设置electron_config_cache环境变量来提供用户自定义缓存目录,在命令行下临时设置这个环境变量的方式为:
> set electron_config_cache=D:\ElectronDeepDive\capture1\cache
如果你是通过编程的方式使用@electron/get包,那么也可以通过如下方式把环境变量的设置写到代码里:
process.env.electron_config_cache="D:\\ElectronDeepDive\\capture1\\cache"
如果你希望一劳永逸的解决这个问题,还可以把这个环境变量配置到操作系统中去,如下图所示:
图1-1 Electron缓存目录环境变量设置
在国内网络环境不理想的情况下,安装Electron npm包十有八九会失败,
这就是Electron的二进制文件压缩包难以下载成功导致的,
知道了缓存目录的位置之后,你就可以先手动把Electron二进制包安放到相应的缓存目录中,
这样再安装Electron npm包时就毫无阻滞了。
你可以从同事的电脑上拷贝相应版本的Electron二进制包,
也可以从淘宝的镜像源手动下载Electron的二进制包,淘宝Electron镜像源的地址为:
https://npm.taobao.org/mirrors/electron/
下载好的压缩包和哈希值文件一定要按照如下路径放置在缓存目录里:
//二进制包文件的路径 [你的缓存目录]/httpsgithub.comelectronelectronreleasesdownloadv9.2.0electron-v9.2.0-win32-x64.zip/electron-v9.2.0-win32-x64.zip //哈希值文件的路径 [你的缓存目录]/httpsgithub.comelectronelectronreleasesdownloadv9.2.0SHASUMS256.txt/SHASUMS256.txt
路径中[你的缓存目录]下的子目录的命名方式看起来有些奇怪,
这其实就是下载地址格式化得来的(通过一个叫做sanitize-filename的工具库,
去除了url路径中的斜杠,使得其能成为文件路径),
在我的电脑上,这两个路径是如下形式:
图1-2 二进制包文件的路径
图1-3 哈希值文件的路径
细心的读者可能已经注意到了,我的路径并不是github的url地址格式化得来的,
而是taobao的镜像源地址格式化得来的。下面我们就介绍一下如何设置第三方镜像源。
@electron/get库下载Electron二进制文件包的地址被人为的分割成了三部分:
- 镜像部分:https://github.com/electron/electron/releases/download/
- 版本部分:v9.2.0/
- 文件部分:electron-v9.2.0-win32-x64.zip
这三部分联合起来最终构成了下载地址,每个部分都有其默认值,也有对应的重写该部分值的环境变量:
- 镜像部分的环境变量:ELECTRON_MIRROR
- 版本部分的环境变量:ELECTRON_CUSTOM_DIR
- 文件部分的环境变量:ELECTRON_CUSTOM_FILENAME
一般情况下,我们只需要设置镜像部分的环境变量即可,
比如要设置淘宝的镜像源,
只需要把ELECTRON_MIRROR的环境变量的值设置为https://npm.taobao.org/mirrors/electron/即可,
设置方式与设置缓存目录的环境变量方式相同,此处不再赘述。
4. 在bin目录下注入命令
Electron依赖包安装完成后,
npm会自动为其在node_modules/.bin路径下注入命令文件,
不带扩展名的electron文件是为linux和mac准备的shell脚本,
electron.cmd是传统的windows批处理脚本,
electron.ps1是运行在windows powershell下的脚本。
命令文件中的脚本代码不多,以electron.cmd为例,我们简单解释一下:
@ECHO off SETLOCAL CALL :find_dp0 IF EXIST "%dp0%\node.exe" ( SET "_prog=%dp0%\node.exe" ) ELSE ( SET "_prog=node" SET PATHEXT=%PATHEXT:;.JS;=;% ) "%_prog%" "%dp0%\..\electron\cli.js" %* ENDLOCAL EXIT /b %errorlevel% :find_dp0 SET dp0=%~dp0 EXIT /b
其中~dp0指执行脚本的当前目录,
SET是为一个变量赋值,
%*是执行命令时输入的参数,
整段命令脚本的意思是用node执行Electron包内的cli.js文件,
并把所有命令行参数一并传递过去。
关于windows批处理的更多细节请参阅(https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands)
细心的读者会发现,npm并不会为所有的依赖包注入命令文件,
而且即使注入了命令文件的包也不一定存在cli.js文件,
比如npm就没有为core-js包注入命令文件,却为Mocha注入了两组命令文件,
Electron或者Mocha的独特之处在于它们的package.json里都有类似如下这样的配置(Mocha为bin对象配置了两个属性,所以npm为其生成了两组指令文件):
"bin": { "electron": "cli.js" }
npm之所以在node_modules/.bin路径下添加命令文件,
是因为很多包的作者都希望自己的脚本能放置在用户的环境变量里。
npm为这个需求提供了便利。npm在执行一段脚本前,
比如:npm run dev,会先自动新建一个命令行环境,然后把当前目录的node_modules/.bin加入到系统环境变量中,
接着执行scripts配置节指定的脚本的内容,
执行完成后再把node_modules/.bin从系统环境变量中删除。
所以当前目录的node_modules/.bin子目录里面的所有脚本,
都可以直接用脚本名调用,而不必加上路径。
当然你的项目的package.json里要配置了dev对应的指令,示例的配置代码如下:
"scripts": { "dev": "electron ./index.js" }
有了上面的配置,你就可以通过运行npm run dev命令来启动你安装过的Electron了。
下面我们就来看看Electron包内的cli.js是如何启动Electron的。
5. 使用命令启动Electron
当开发者在当前项目下执行npm run dev时,其实就是执行electron.cmd批处理文件,
并传入了一个命令行参数./index.js
(这个文件我们还没有创建,不过没关系,在这一小节里这个文件并不是重点)。
我们知道electron.cmd批处理指令就是用node执行了node_modules\electron\cli.js文件,
同时也把命令行参数复制过去了。那么我们就看看cli.js的执行逻辑。
cli.js中最重要的逻辑代码如下(为了便于理解,我对这段代码略有改动):
var proc = require('child_process') var child = proc.spawn(electronExePath, process.argv.slice(2), { stdio: 'inherit', windowsHide: false })
这段代码就是使用Node.js的child_process对象创建了一个子进程,
让子进程执行Electron的可执行文件,并把当前进程的命令行参数传递给了这个子进程。
命令行参数之所以从第三位开始取,是因为按照Node.js的约定,process.argv的第一个值为process.execPath,
第二个值为正被执行的 JavaScript 文件的路径,所以第三个值才是我们需要的./index.js。
值得注意的是cli.js文件的首行代码:
#!/usr/bin/env node
这行代码是一个Shebang行(https://en.wikipedia.org/wiki/Shebang_(Unix)),
是类Unix平台上的可执行纯文本文件中的第一行,
通过#!前缀后面的命令行告诉系统将该文件传递给哪个解释器以供执行。
虽然Windows不支持Shebang行,但因为这是npm的约定,所以这一行代码仍然是必不可少的。
至于Electron的可执行程序是如何接收这个参数,如何执行这个参数指向的程序文件的,我们后文会有详细描述。
6. Electron的版本管理方式
自Electron 2.0.0以来,Electron的版本管理方式遵循semver的管理规则,
semver是 语义化版本规范(https://semver.org/lang/zh-CN/)的一个实现,
这是一个由npm的团队维护的版本管理规范,它实现了版本和版本范围的解析、计算、比较。
semver的版本号内容分为主版本号、次版本号和修订号三个部分,中间以点号分割,
版本号递增规则如下:
- 主版本号:当做了不兼容的修改时递增;
- 次版本号:当做了向下兼容的功能性更新时递增;
- 修订号:当做了向下兼容的问题修正时递增;
Electron则在这个约束的前提下增加了如下递增规则:
主版本号更新规则 |
次版本号更新规则 |
修订号更新规则 |
Electron有不兼容的修改时递增 |
Electron兼容性更新时递增 |
Electron问题修复时递增 |
Node.js主版本号更新时递增 |
Node.js次版本号更新时递增 |
Node.js修订版本号更新时递增 |
Chromium更新时递增 |
|
为Chromium打补丁时更新 |
推荐大家使用稳定状态的最新版本的Electron,
如果已经安装了老版本的Electron或者发现Electron有可用的更新
(关注Electron官网的发布页面:https://www.electronjs.org/releases/stable可获得更新信息),
大家可以使用如下指令更新本地工程的Electron版本:
npm install --save-dev electron@latest
Electron团队承诺只维护最近的三个大版本,
比如本文发稿时Electron最新版本为v9.2.0,
那么Electron团队只会维护v9.x.x,v8.x.x,v7.x.x。v6.x.x则不再维护,当v10.x.x发布之后,v7.x.x也不再维护了。
而且目前Electron版本发布相当频繁,平均一到两周就会有一个新的稳定版本发布,
大量的更新不仅仅带来了更多的新功能、解决了更多的问题,
也意味着你所使用的版本即将成为无人理睬的版本了,这也是为什么我推荐大家紧跟官方团队版本发布步伐的原因。
我们通过npm包管理工具安装的Electron依赖包都是稳定版本,
除稳定版本外,Electron团队还维护着beta版本和nightly版本,
这是Electron团队和一些激进的开发者的演武场,除非特别需要,不推荐在商业项目中使用这些版本。
另外Electron官方github仓储的issue页面(https://github.com/electron/electron/issues)也时常会置顶一些重要更新事项,
与社区的开发者一起讨论更新方案的细节,等最终方案敲定后,则逐步推动更新落地。
比如近期非常重要的移除remote模块的更新需求,就是置顶在这个页面的。
这是开发者持续关注官方动向的最佳途径,
一旦发现有不赞成(deprecate)的内容或破坏性(breaking)的更新被置顶在这个页面,
就应该尽量在项目当中避免使用它们。
最后,推荐一下我自己的新书《Electron实战》
http://product.dangdang.com/28547952.html