深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

1.5 注入命令

当开发者执行npm run dev指令时,npm会自动新建一个命令环境,然后把当前项目下的node_modules/.bin目录加入到这个命令环境的环境变量中,接着再执行scripts配置节点dev指定的脚本内容。执行完成后,再把node_modules/.bin从这个环境变量中删除。

在npm自动创建的命令环境下可以直接用脚本名调用node_modules/.bin子目录里面的所有脚本,而不必加上路径。

打开node_modules/.bin目录,你就会发现有三个文件是与我们的Electron依赖包有关的:electron、electron.cmd、electron.ps1。前面执行npm run dev指令时会在对应的命令行下执行electron./index.js指令,背后实际上执行的是electron.cmd./index.js,Windows操作系统下可以省略命令行下的扩展名.cmd。

那么这三个文件是哪来的呢?

Electron依赖包安装完成后,npm除了会检查钩子脚本外,还会检查Electron依赖包内package.json文件中是否配置了bin指令,很显然Electron依赖包是配置了这个指令的,代码如下所示:

"bin": {
  "electron": "cli.js"
}

一旦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

整段批处理脚本的意义就是用Node.js执行Electron包内的cli.js文件,并把用户输入的所有命令行参数一并传递给cli.js(实际上是传递给Node.js的可执行程序)。这段批处理脚本中,~dp0指脚本所在的目录,SET是为一个变量赋值,%*是执行命令时输入的参数。关于Windows批处理的更多细节请参阅https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands

也就是说,执行electron.cmd./index.js其实就是执行了如下命令:

> node /node_modules/electron/cli.js ./index.js

这就是执行npm run dev指令时npm真正为我们执行的指令。

细心的读者可能会发现,npm并不会为所有的依赖包注入命令文件,因为大部分包都没有配置bin指令,这很容易理解,但有些包却有两组命令文件,比如Mocha(一个测试工具包),这是因为它在package.json中增加了两组指令,代码如下所示:

"bin": {
  "mocha": "./bin/mocha",
  "_mocha": "./bin/_mocha"
},

下面我们就来看看Electron包内的cli.js是如何启动Electron的。

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.slice(2)的意义是把命令行参数数组的前两位去掉,命令行参数之所以从第三位开始取,是因为按照约定,process.argv的第一个值为当前可执行程序的路径,也就是Node.exe的路径,这对于我们来说是无意义的。第二个值为正被执行的JavaScript文件的路径,也就是cli.js的路径,这也是无意义的。第三个值才是我们需要的,也就是./index.js。

electronExePath是一个文件路径变量,它指向的就是[yourProjectPath]\node_modules\electron\dist\electron.exe。

也就是说这段代码的意义是启动Electron的可执行程序,并在启动时为它传递了一个命令行参数./index.js,实际上就是让electron.exe解释并执行index.js内的JavaScript代码。至此npm和Node.js的使命就完成了,后面的任务都是在Electron内部完成了,至于Electron内部如何加载index.js,如何为用户的代码提供各类API,我们将在后面详细讲解。

值得注意的是cli.js文件的首行代码:

#!/usr/bin/env node

这行代码是一个Shebang行(https://en.wikipedia.org/wiki/Shebang_(Unix)),是类UNIX平台上的可执行纯文本文件中的第一行,它通过“#!”前缀后面的命令行告诉系统将该文件传递给哪个解释器以供执行。虽然Windows不支持Shebang行,但因为这是npm的约定,所以这一行代码仍然是必不可少的。

这就是Electron依赖包启动Electron可执行程序的全过程。