硬件
以前,Web前端技术是没办法访问客户机内的硬件设备的,比如音视频设备、电源设备等。
后来HTML5提供了一系列的技术来弥补这项不足,但安全限制颇多,一旦网页尝试使用这些硬件设备,则弹出用户授权窗口,用户授权后网页才有能力访问这些设备。
Electron可以自由地使用这些技术,而且默认拥有这些硬件的访问权限,Electron内部甚至还提供了额外的支持以帮助开发者使用更多的硬件能力。
屏幕
获取扩展屏幕
客户电脑有可能外接了多个显示器,获取这些显示器的信息可以帮助开发者确定把应用窗口显示在哪个屏幕上,以及显示在屏幕的具体哪个位置上。
我们在“菜单”章节讲解的以窗口形式创建的菜单案例,就需要判断这个菜单窗口应该显示在哪个显示器上。
Elecron内置了API以支持获取主显示器及外接显示器的信息。如下代码可以获取主显示器信息:
1 2 3 |
|
mainScreen是一个显示器信息对象,它包含很多字段,主要的字段如下:
- id:显示器ID。
- rotation:显示器是否旋转,值可能是0、90、180、270。
- touchSupport:是否为触屏。
- bounds:绑定区域,可以根据此来判断是否为外接显示器。
- size:显示器大小,与显示器分辨率有关,但并不一定是显示器分辨率。
下面的示例代码,控制窗口显示在外接显示器上:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
以上代码先通过 screen.getAllDisplays
方法获取到所有显示器信息,再通过显示器信息的 bounds 对象来判断是否为外接显示器(如果bounds.x
或 bounds.y
不等于 0 则认为是外接显示器),接着把窗口显示在外接显示器屏幕的左上角。
重点:
虽然显示器信息对象包含internal属性,官方说明此属性值为true是内置显示器,值为false为外接显示器。
但实验证明,无论是内置显示器还是外接显示器,此值都为 false。因此通过 display.bounds
来确定是否为外接显示器更准确。
另外 screen 模块只有在 app.ready
事件发生之后才能使用。
在 6.x.y 及以前的版本中,你甚至不能在主进程代码首行引入此模块,只能在 app.ready
事件发生时或之后的时间线上引入。
在自助服务机中使用 Electron
目前国内大多数自助服务机同时也是一台PC机,内部安装的是Windows或者Linux操作系统,近年来才逐渐有安卓系统的自助服务机。
如果一个自助服务机内安装的是Windows或者Linux操作系统,那么它就可以使用基于Electron开发的应用作为其提供服务的载体。
自助服务机内的应用与传统PC端桌面应用的不同之处在于以下两点:
- 它们大部分不允许用户主动退出应用。
- 它们大部分都是支持触屏的应用。
在创建窗口时,会有一个专门为自助服务机配置的kiosk参数,把此参数设置为true,
窗口打开后将自动处于全屏状态,系统桌面的任务栏和窗口的默认标题栏都不会显示(开发者自定义的标题栏除外)。
再结合按键控制及守护进程,就能有效地防止用户主动退出应用了,代码如下:
1 2 3 4 5 6 |
|
开启kiosk参数后,窗口的高度和宽度设置将失效。
在Windows或Linux系统上开启kiosk参数与开启fullscreen参数效果是一样的,它们的内部实现原理也是一样的。
如果你开发的是一款需要全屏显示的游戏应用,也可以使用kiosk参数来控制全屏。
开发者应考虑提供给程序退出kiosk模式的机制,比如测试和运维人员可能就需要这个功能来检查程序是否正常运行。
我们可以考虑为应用程序提供登录和角色验证机制,核准当前用户为测试或运维人员后,控制应用退出kiosk模式的按钮可见。
扩展:
此类应用运行环境比较特殊(自助服务机内部往往是一台工控机,其性能有限但对恶劣的外部环境适应力较高),如果应用自身代码比较复杂,难免会产生异常,导致程序退出。
此时如果有一个稳定的守护进程处于监视状态,一旦发现应用程序退出,马上重启该应用,就能有效地避免自助服务机中断服务。
另外,如果自助服务机支持全键盘,应想办法禁用Ctrl+Alt+Del快捷键。
因为此快捷键会使应用程序处于被用户手动退出的风险中。
Electron没有内置此支持,因为一旦它这么做了,很有可能会被杀毒软件检测为病毒程序。
又及,有一些自助服务机还在使用Windows XP系统,Electron是不支持Windows XP系统的。
如果你没办法使自助服务机升级操作系统的话,请考虑使用其他技术方案(NW.js的早期版本是支持Windows XP的,但我不建议使用)。
Electron应用默认支持触屏设备,无需做额外的设置。
触屏应用一般不会显示鼠标指针,开发者可以通过如下样式隐藏界面的鼠标指针:
1 |
|
另外,你可以通过如下API把鼠标锁定在窗口可见区域内:
1 |
|
如果需要取消鼠标锁定,可以使用如下API:
1 |
|
有的时候我们需要在自助服务机上打开系统的软键盘,为满足此需求需要用到Node.js子进程的技术,代码如下:
1 2 |
|
软键盘打开后如图10-1所示。
图10-1 Windows软键盘
音视频设备
使用摄像头和麦克风
在开发Web网页时,如果要使用用户的音视频设备,浏览器为了安全,会向用户发出提示,如图10-2所示。
图10-2 网页访问音视频时的授权申请
用户允许浏览器访问其音视频设备后,前端代码才有权访问这些设备。而Electron不必获得用户授权,直接具有访问用户音视频设备的能力,代码示例如下:
1 2 3 4 5 6 7 8 9 10 |
|
在示例代码中,我们使用navigator.mediaDevices.getUserMedia来获取用户的音视频流。
getUserMedia方法需要一个配置参数,该配置参数有两个属性audio和video。如果你只希望获取两者之一,那么只要把另外一项设置为false即可。
你可以设置视频的宽度和高度,也可以设置视频的来源是前置摄像头还是后置摄像头,此时要把option对象的video属性设置为一个对象,如下代码所示:
1 2 3 4 5 6 |
|
如果你的设备有多个摄像头,并且不区分前后,那么你可以通过如下方法获取到所有摄像头设备的基本信息:
1 |
|
返回的devices是一个如下形式的数组:
1 2 3 4 5 6 7 8 |
|
数组中不但包括视频设备,还包括音频设备,你可以根据deviceId来指定需要获取哪个音视频设备的数据。参数配置如下:
1 |
|
上面的配置参数,当 myPreferredCameraDeviceId
的设备不可用时,系统会随机返回给你一个可用的设备。
你如果只想用你指定的设备,可以用如下配置:
1 |
|
以此配置访问设备,当设备不可用时,将抛出异常。
录屏
除了获取音视频设备传递的媒体流之外,你还可以通过 Electron 的 desktopCapturer
模块提供的API获取桌面应用的屏幕视频流。
以下代码可以获取到微信窗口的视频流并显示在应用内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
其中 desktopCapturer.getSources
获取所有显示在桌面上的应用信息,获取到指定应用后,我们把应用的ID传递给video.mandatory.chromeMediaSourceId,同时设置了video.mandatory.chromeMediaSource的值为desktop。
此后我们得到的视频流对象就与从摄像头里得到的视频流对象基本一致了。
代码执行后效果如图10-3所示。
图10-3 窗口录屏示例
电源
电源的基本状态和事件
可以通过如下代码获取到电源管理器(BatteryManager)的实例:
1 |
|
通过该实例可以获知当前电源的状态信息,监听电源充电状态变化的事件,如表10-1、表10-2所示。
表10-1 batteryManager 实例下的状态属性
属性名称 | 属性说明 |
---|---|
charging | 是否正在充电,只要正在充电,即使当前是满电状态,此值也为 true |
chargingTime | 距离电池充满还剩多少时间,单位为秒,如果为 0,则当前电池是满电状态 |
dischargingtTime | 电池电量用完所需时间,单位为秒,如果电池是满电状态且正在充电,其值为 infinity(无限长) |
level | 代表充电水平,其值在 0 到 1 之间 |
表10-2 batteryManager实例的可控事件
事件名称 | 事件说明 |
---|---|
onchargingchange | 接入交流电和断开交流电时触发该事件 |
onchargingtimeChange | 当 chargingTime 属性变化时,触发该事件 |
ondischargingtimechange | 当 dischargintTime 属性变化时,触发该事件 |
onlevelchange | 当 level 属性变化时,触发该事件 |
监控系统挂起与锁屏事件
上面介绍了 HTML5为网页提供的关于电源设备的API。
Electron应用除了可以使用HTML5 API的能力外,自己也封装了关于电源的powerMonitor模块,并且把监控系统是否挂起和恢复的事件、系统空闲状态获取的能力也放在了这个模块中。
如下代码演示了如何监视系统挂起和恢复的事件:
1 2 3 4 5 6 7 |
|
当系统睡眠时触发powerMonitor模块的suspend事件,系统从睡眠中恢复时触发powerMonitor模块的resume事件。
除此之外,powerMonitor模块还可以监控屏幕锁定'lock-screen'和屏幕解锁'unlock-screen'事件,但这两个事件只适用于Mac系统和Windows系统,Linux系统下暂无法使用,代码如下:
1 2 3 4 5 6 |
|
以上四个事件在一些特殊的场景下非常有用,因为在系统挂起后,系统内一些应用会切换到挂起状态,不再提供服务。
如果Electron应用是依赖这些服务的,那么很有可能会出现业务问题。通过监控这些事件,我们可以让Electron应用提前做一些准备,避免业务异常。
另外,无论挂起还是锁屏,都代表用户已经离开了应用,此时与用户交互的界面也应离开用户,比如游戏应用中考虑暂停角色受到伤害等。
阻止系统锁屏
操作系统在长时间没有收到用户鼠标或键盘事件时,会进入省电模式:关闭用户的显示器,把内存中的内容转储到磁盘,进入睡眠模式。
在一些特殊的场景中,用户是不希望操作系统息屏式进入睡眠模式的,比如用户看电影、演示文稿或游戏挂机时。
为此,操作系统为应用程序提供了阻止系统息屏、睡眠的API,Electron也有访问这个API的能力,请看如下代码:
1 2 |
|
执行上面的代码,即可阻止系统息屏。
powerSaveBlocker.start方法接收一个字符串参数,prevent-display-sleep阻止系统息屏,prevent-app-suspension阻止应用程序挂起(应用程序在下载文件或播放音乐时需要阻止应用挂起)。
powerSaveBlocker.start方法返回一个整型的id值,如果应用在某个时刻不再需要阻止系统进入省电模式,可以使用powerSaveBlocker.stop(id);来取消阻止行为,你也可以通过powerSaveBlocker.isStarted(id)来判断阻止行为是否已经启动。
打印机
控制打印行为
打印是日常办公中常见的需求。Electron支持把webContents内的内容发送至打印机进行打印,下面是打印webContents内容的代码:
1 2 3 4 5 6 7 8 9 10 |
|
如你所见,调用webContents对象的print方法即可打印当前页面的内容。
执行上面的代码,系统提示你选择打印机,如图10-4所示。
图10-4 选择打印机
如果你希望通过程序指定一个打印机,那么你应该先获得连接当前电脑的所有打印机,并找出你要使用的打印机,代码如下:
1 2 3 4 5 6 |
|
以上程序执行后,在我的电脑上输出如下内容:
1 2 3 4 |
|
选择其中一个,设置为webContents.print方法的参数的deviceName属性,然后把silent属性设置为true,即可跳过配置打印机的环节,直接打印内容。
打印成功之后,直接进入webContents.print的回调函数,回调函数success参数为true;如果打印失败,或者用户取消打印,success参数为false。
导出 PDF
开发者可以利用webContents的打印能力把页面内容以PDF文件形式导出,如下代码所示:
1 2 3 4 5 6 7 8 9 10 |
|
webContents.printToPDF
接收一个配置参数,其与print接收的配置参数类似,这里不再详述。
此方法返回一个Promise对象,其内容是PDF的Buffer缓存,开发者可以直接把这个Buffer保存到指定的文件路径。
也可以打开保存文件对话框,让用户选择保存文件的路径,代码如下所示。
1 2 3 4 5 6 7 8 9 |
|
提示用户选择路径的对话框如图10-5所示,注意,我们在程序中设置了保存文件的类型。
图10-5 保存PDF文件
硬件信息
获取目标平台硬件信息
Electron提供了几个简单的API来获取用户硬件的使用情况,比如获取内存的使用情况,代码如下:
1 2 |
|
以上代码输出结果为:
1 |
|
其中total为当前系统可用的物理内存总量;free为应用程序或磁盘缓存未使用的内存总量;
swapTotal为系统交换内存总量;swapFree为系统可用交换内存总量。
单位均为KB。
获取CPU的使用情况的代码如下:
1 2 3 4 5 |
|
process.getCPUUsage方法返回一个CPUUsage对象,这个对象有两个属性:percentCPUUsage代表着某个时间段内的CPU使用率;
idleWakeupsPerSecond代表着某个时间段内每秒唤醒空闲CPU的平均次数,此值在Windows环境下会永远返回0。
第一次调用process.getCPUUsage方法时这两个值均为0,后面每次调用获得的值为本次调用与上次调用之间这段时间内相应的CPU使用率和唤醒空闲CPU的平均次数,所以在上面的示例代码中,我使用了一个定时器来不断地请求CPUUsage对象。
当我们研发一些深度依赖客户机器硬件的专有软件时,往往需要获得更多更详细的硬件信息,此时Electron提供的API就显得力不从心了,为此我推荐你使用systeminformation(https://github.com/sebhildebrandt/systeminformation
)这个库来获取更详尽的硬件信息。
systeminformation是一个JavaScript包,所以安装到Electron项目中并不需要额外的配置,通过这个工具库获取系统主要硬件信息的演示代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
如你所见,这个库提供了便于开发者使用的Promise API,所以我们把获取硬件信息的操作包装在一个async标记的立即执行函数里了。
如果想了解返回的硬件信息的具体含义,读者可以参阅 systeminformation 库的官方文档 https://systeminformation.io/general.html
。
使用硬件串号控制应用分发
开发一个商业桌面GUI应用,有的时候需要控制应用分发的范围,比如用户购买某软件的使用权后,应用开发商只允许用户在某一台固定的物理设备上使用该软件,不允许用户随意地在其他设备上安装并使用。
如果用户希望在另一台设备上也可以使用,则需要另外购买使用权。
开发者常用的WebStorm或Visual Studio Ultimate等软件都有类似的限制。
如果开发者想让自己开发的软件具备这个能力,一种常见的办法是获取用户设备的专有硬件信息,并把这个信息与当前使用该软件的用户信息、用户的付费情况信息绑定,保存在一个服务器上。
当用户打开软件时,软件获取这个物理设备的专有硬件信息,并把这个信息连同当前用户信息发送到服务器端,由服务器端确认该用户是否已为该设备购买了授权,如果没有则通知应用,要求用户付费。
这种方式有两个弊端,一是由于验证过程强依赖于服务端,所以这个应用必须联网,对于一些有离线使用需求的应用来说,这个方案显然行不通。
除此之外,一些恶意用户完全可以自己开发一个简单的服务,代理这个验证授权的请求,这个简单的服务只要永远返回验证授权通过的结果就能让恶意用户免费使用该软件。
对于离线应用来说,开发者可以通过一个安全的算法来保证应用只被安装在一台设备上,具体实现过程为:当应用第一次启动时,应用获取到这个物理设备的专有硬件信息,并把这个信息发送到服务端,用户付费后,服务端通过算法生成一个与该硬件信息匹配的激活码,并把这个激活码发送给用户,由用户把激活码保存在应用内。
以后用户每次启动应用时,应用通过同样的算法验证激活码是否与当前设备的硬件信息匹配,如果匹配则授权成功,反之授权失败。
这个执行逻辑的关键点是服务端根据用户设备的专有硬件信息生成激活码的过程,和应用每次启动时检验激活码与硬件信息是否匹配的过程。
这两个过程内的算法是要严格保密的,一旦被恶意用户窃取(或通过逆向工程分析出了算法的逻辑),那么恶意用户就可以开发一个注册机来无限制地生成激活码,无限制地分发你的软件。
著名的商业软件Photoshop就备受这种恶意注册机的困扰。
WebStorm支持以上两种形式的授权过程,如图10-6中①和②处所示。
图10-6 WebStorm 用户授权窗口界面
无论选用什么方法,应用都要获取用户物理设备的专有硬件信息,而且得到的这个信息不能与其他物理设备重复,所以设备的厂商、磁盘的大小、CPU的频率等这类信息都不能使用。
对我们最有帮助的就是设备串号,设备串号是设备生产厂家保证当前设备全球唯一的一个字符串(理论上唯一,在某些特殊情况下也无法保证唯一)。
我们可以通过上一节推荐的systeminformation库来获取当前硬件中各个组件的串号,并依据这些串号的组合来保证硬件专有信息唯一,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
si.getStaticData方法返回当前物理设备的所有组件的静态信息,包括内存、CPU、磁盘网卡等组件的生产厂家、型号、硬件串号等信息。
通过上述代码得到硬件串号字符串之后,最好不要直接使用它,而是对它进行一次哈希运算,取得这个硬件串号字符串的哈希值,代码如下所示:
1 2 3 |
|
我们可以认为serialHash字符串就代表一台固定的物理设备。
得到这个哈希字符串后我们把它与用户的付费信息一起保存在服务端,每次用户启动应用时,首先检验这个哈希值对应的用户是否已为这台设备付费,如果没有付费,则提醒用户付费。
扩展:
哈希算法可以把任意长度的输入通过散列算法变换成固定长度的输出,这个输出就是哈希值,哈希值和源数据的每一个字节都有十分紧密的关系。另外,哈希算法是很难找到逆向规律的。
如果开发者开发的是可以离线使用的应用程序,那么可以依据此哈希值生成对应的激活码,当用户付费后,用户将获得一个激活码,
这个激活码与硬件串号哈希值存在算法上的相关关系,应用每次启动时将通过算法检查这个关系是否存在,如果存在则证明此应用可以正常使用,如果不存在,则提示用户付费。
重点:
有些开发者直接使用硬件串号的哈希值作为激活码,应用每次启动时得到硬件串号字符串后,即对其进行哈希运算,检查运算后的哈希值是否与激活码相同,以此来验证软件是否付费。
如果你能确保恶意用户永远不会知道你是这么做的,那这也不失为一个办法,但我建议你在计算哈希值时为硬件串号加“盐”,以提升安全程度,代码如下所示:
1 |
|
这里 [solt]
就是你的“盐”值,在保证算法不被窃取的同时,也需要保证“盐”值不被窃取,这样即使恶意用户知道你是使用sha256算法对硬件串号进行哈希运算的,也无法顺利地开发出注册机来侵犯你的权益。
通过算法来确保授权过程安全可靠的方法也可以用在基于网络验证授权的方案中,当服务器返回授权成功或授权失败的信息后,给这个信息增加一个随机数值(往往为一个基于时间种子的随机数),然后再加密返回给客户端。
客户端收到加密信息后,解密还原出具体的授权信息,由于有随机数的存在,客户端每次请求这个授权验证服务时返回的结果都是不同的,就算恶意用户发现了这个授权验证的服务地址,在不知道你的算法的前提下,也无法制作他自己的授权验证服务。