https://uinika.github.io/web/server/electron.html
早期桌面应用的开发主要借助原生 C/C++ API 进行,由于需要反复经历编译过程,且无法分离界面 UI 与业务代码,开发调试极为不便。后期出现的 QT 和 WPF 在一定程度上解决了界面代码分离和跨平台的问题,却依然无法避免较长时间的编译过程。近几年伴随互联网行业的迅猛发展,尤其是 NodeJS、Chromium 这类基于 W3C 标准开源应用的不断涌现,原生代码与 Web 浏览器开发逐步走向融合,Electron 正是在这种背景下诞生的。
Electron 是由 Github 开发,通过将Chromium和NodeJS整合为一个运行时环境,实现使用 HTML、CSS、JavaScript 构建跨平台的桌面应用程序的目的。Electron 源于 年 Github 社区提供的开源编辑器 Atom,后于 年在社区开源,并在 年的 5 月和 8 月,通过了Mac App Store和Windows Store的上架许可,VSCode、Skype 等著名开源或商业应用程序,都是基于 Electron 打造。为了方便编写测试用例,笔者在 Github 搭建了一个简单的 Electron 种子项目Octopus,读者可以基于此来运行本文涉及的示例代码。
Getting Start
首先,让我们通过npm init
和git init
新建一个项目,然后通过如下npm
语句安装最新的 Electron 稳定版。
然后向项目目录下的package.json
文件添加一条scripts
语句,便于后面通过npm start
命令启动 Electron 应用。
然后在项目根目录下新建resource
文件夹,里面分别再建立index.html
和main.js
两个文件,最终形成如下的项目结构:
main.js
是 Electron 应用程序的主入口点,当在命令行运行这段程序的时候,就会启动一个 Electron 的主进程,主进程当中可以通过代码打开指定的 Web 页面去展示 UI。
Web 页面index.html
运行在自己的渲染进程当中,但是能够通过 NodeJS 提供的 API 去访问操作系统的原生资源(例如下面代码中的process.versions
语句),这正是 Electron 能够跨平台执行的原因所在。
使用命令行工具执行npm start
命令之后,上述 HTML 代码在笔者 Linux 操作系统内被渲染为如下界面。应用当中,可以通过CTRL+R
重新加载页面,或者使用CTRL+SHIFT+I
打开浏览器控制台。
一个 Electron 应用的主进程只会有一个,渲染进程则会有多个。
主进程与渲染进程
主进程(main process)管理所有的 web 页面以及相应的渲染进程,它通过BrowserWindow
来创建视图页面。渲染进程(renderer processes)用来运行页面,每个渲染进程都对应自己的BrowserWindow
实例,如果实例被销毁那么渲染进程就会被终止。Electron 分别在主进程和渲染进程提供了大量 API,可以通过require
语句方便的将这些 API 包含在当前模块使用。但是 Electron 提供的 API 只能用于指定进程类型,即某些 API 只能用于渲染进程,而某些只能用于主进程,例如上面提到的BrowserWindow
就只能用于主进程。
Electron 通过remote
模块暴露一些主进程的 API,如果需要在渲染进程中创建一个BrowserWindow
实例,那么就可以借助这个remote
模块:
Electron 可以使用所有 NodeJS 上提供的 API,同样只需要简单的require
一下。
当然,NodeJS 上数以万计的 npm 包也同样在 Electron 可用,当然,如果是涉及到底层 C/C++的模块还需要单独进行编译,虽然这样的模块在 npm 仓库里并不多。
既然 Electron 本质是一个浏览器 + 跨平台中间件
的组合,因此常用的前端调试技术也适用于 Electron,这里可以通过CTRL+SHIFT+I
手动开启 Chromium 的调试控制台,或者通过下面代码在开发模式下自动打开:
核心模块
本节将对require("electron")
所获取的模块进行概述,便于后期进行分类查找。
app 模块
Electron 提供的app模块即提供了可用于区分开发和生产环境的app.isPackaged
属性,也提供了关闭窗口的app.quit()
和用于退出程序的app.exit()
方法,以及window-all-closed
和ready
等 Electron 程序事件。
可以使用
app.getLocale()
获取当前操作系统的国际化信息。
BrowserWindow 模块
工作在主进程,用于创建和控制浏览器窗口。
例如需要创建一个无边框窗口的 Electron 应用程序,只需将BrowserWindow
配置对象中的frame
属性设置为false
即可:
例如加载页面时,渲染进程第一次完成绘制时BrowserWindow
会发出ready-to-show
事件。
对于较为复杂的应用程序,ready-to-show
事件的发出可能较晚,会让应用程序的打开显得缓慢。 这种情况下,建议通过backgroundColor
属性设置接近应用程序背景色的方式显示窗口,从而获取更佳的用户体验。
如果想要创建子窗口,那么可以使用parent
选项,此时子窗口将总是显示在父窗口的顶部。
创建子窗口时,如果需要禁用父窗口,那么可以同时设置modal
选项。
globalShortcut 模块
使用globalShortcut
模块中的register()
方法注册快捷键。
Linux 和 Windows 上【Command】键会失效, 所以要使用 CommandOrControl(既 MacOS 上是【Command】键 ,Linux 和 Windows 上是【Control】键)。
clipboard 模块
用于在系统剪贴板上执行复制和粘贴操作,包含有readText()
、writeText()
、readHTML()
、writeHTML()
、readImage()
、writeImage()
等方法。
globalShortcut 模块
用于在 Electron 应用程序失去键盘焦点时监听全局键盘事件,即在操作系统中注册或注销全局快捷键。
ipcMain 与 ipcRenderer 模块
用于主进程到渲染进程的异步通信,下面是一个主进程与渲染进程之间发送和处理消息的例子:
如果需要完成渲染器进程到主进程的异步通信,可以选择使用
ipcRenderer
对象。
Menu 与 MenuItem 模块
用于主进程,用于创建原生应用菜单和上下文菜单。
使用
MenuItem
类可以添加菜单项至 Electron 应用程序菜单和上下文菜单当中。
netLog 模块
用于记录网络日志。
powerMonitor 模块
通过 Electron 提供的powerMonitor
模块监视当前电脑电源状态的改变,值得注意的是,在app
模块的ready
事件被触发之前, 不能引用或使用该模块。
powerSaveBlocker 模块
阻止操作系统进入低功耗 (休眠) 模式。
protocol 模块
注册自定义协议并拦截基于现有协议的请求,例如下面代码实现了一个与[file://]
协议等效的示例:
net 模块
net
模块是一个发送 HTTP(S) 请求的客户端 API,类似于 NodeJS 的 HTTP 和 HTTPS 模块 ,但底层使用的是 Chromium 原生网络库。
Electron 中提供的
ClientRequest
类用来发起 HTTP/HTTPS 请求,IncomingMessage
类则用于响应 HTTP/HTTPS 请求。
remote 模块
remote
模块返回的每个对象都表示主进程中的一个对象,调用这个对象实质是在发送同步进程消息。因为 Electron 当中 GUI 相关的模块 (如dialog
、menu
等) 仅在主进程中可用, 在渲染进程中不可用,所以remote
模块提供了一种渲染进程(Web 页面)与主进程(IPC)通信的简单方法。remote 模块包含了一个remote.require(module)
remote.process
:主进程中的process
对象,与remote.getGlobal("process")
作用相同, 但结果已经被缓存。remote.getCurrentWindow()
:返回BrowserWindow
,即该网页所属的窗口。remote.getCurrentWebContents()
:返回WebContents
,即该网页的 Web 内容remote.getGlobal(name)
:该方法返回主进程中名为name
的全局变量。remote.require(module)
:返回主进程内执行require(module)
时返回的对象,参数module
指定的模块相对路径将会相对于主进程入口点进行解析。
remote
模块提供的主进程与渲染进程通信方法比ipcMain
/ipcRenderer
更加易于使用。
screen 模块
检索有关屏幕大小、显示器、光标位置等信息,应用的ready
事件触发之前,不能使用该模块。下面的示例代码,创建了一个可以自动全屏窗口的应用:
shell 模块
提供与桌面集成相关的功能,例如可以通过调用操作系统默认的应用程序管理文件或Url
。
systemPreferences 模块
获取操作系统特定的偏好信息,例如在 Mac 下可以通过下面代码获取当前是否开启系统 Dark 模式的信息。
Tray 模块
用于主进程,添加图标和上下文菜单至操作系统通知区域。
webFrame 模块
定义当前网页渲染的一些属性,比如缩放比例、缩放等级、设置拼写检查、执行 JavaScript 脚本等等。
session 模块
Electron 的session
模块可以创建新的session
对象,主要用来管理浏览器会话、cookie、缓存、代理设置等等。
如果需要访问现有页面的session
,那么可以通过BrowserWindow
对象的webContents
的session
属性来获取。
Electron 里也可以通过session
模块的cookies
属性来访问浏览器的 Cookie 实例。
使用Session
的WebRequest
属性可以访问WebRequest
类的实例,WebRequest
类可以在 HTTP 请求生命周期的不同阶段修改相关内容,例如下面代码为 HTTP 请求添加了一个User-Agent
协议头:
desktopCapturer 模块
用于捕获桌面窗口里的内容,该模块只拥有一个方法:desktopCapturer.getSources(options, callback)
。
options
对象types
:字符串数组,列出需要捕获的桌面类型是screen
还是window
。thumbnailSize
:媒体源缩略图的大小,默认为150x150
。callback
回调函数,拥有如下 2 个参数:error
:错误信息。sources
:捕获的资源数组。
如下代码工作在渲染进程当中,作用是将桌面窗口捕获为视频:
dialog 模块
调用操作系统原生的对话框,工作在主线程,下面示例展示了一个用于选择多个文件和目录的对话框:
由于对话框工作在 Electron 的主线程上,如果需要在渲染器进程中使用, 那么可以通过remote
来获得:
contentTracing 模块
从 Chromium 收集跟踪数据,从而查找性能瓶颈。使用后需要在浏览器打开chrome://tracing/
页面,然后加载生成的文件查看结果。
webview 标签
Electron 的<webview>
标签基于 Chromium,由于开发变动较大官方并不建议使用,而应考虑<iframe>
或者 Electron 的BrowserView
等选择,或者完全避免在页面进行内容嵌入。
<webview>
与<iframe>
最大不同是运行于不同的进程当中,Electron 应用程序与嵌入内容之间的所有交互都是异步进行的,这样可以保证应用程序与嵌入内容双方的安全。
webContents 属性
webContents
是BrowserWindow
对象的一个属性,负责渲染和控制 Web 页面。
window.open() 函数
该函数用于打开一个新窗口并加载指定url
,调用后将会为该url
创建一个BrowserWindow
实例,并返回一个BrowserWindowProxy
对象,但是该对象只能对打开的url
页面进行有限的控制。正常情况下,如果希望完全控制新窗口,可以直接创建一个新的BrowserWindow
。
BrowserWindowProxy
对象拥有如下属性和方法:
win.closed
:子窗口关闭后设置为true
的布尔属性。win.blur()
:将焦点从子窗口中移除。win.close()
:强制关闭子窗口, 而不调用其卸载事件。win.eval(code)
:code
字符串,需要在子窗口 Eval 的代码。win.focus()
:聚焦子窗口(即将子窗口置顶)。win.print()
:调用子窗口的打印对话框。win.postMessage(message, targetOrigin)
:向子窗口发送信息。
Electron 进程
Electron 的process
对象继承自 NodeJS 的process
对象,但是新增了一些有用的事件、属性、方法。
沙箱机制
Chromium 通过将 Web 前端代码放置在一个与操作系统隔离的沙箱中运行,从而保证恶意代码不会侵犯到操作系统本身。但是 Electron 中渲染进程可以调用 NodeJS,而 NodeJS 又需要涉及大量操作系统调用,因而沙箱机制默认是禁用的。
某些应用场景下,需要运行一些不确定安全性的外部前端代码,为了保证操作系统安全,可能需要开启沙箱机制。此时首先在创建BrowserWindow
时传入sandbox
属性,然后在命令行添加--enable-sandbox
参数传递给 Electron 即可完成开启。
使用
sandbox
选项之后,将会阻止 Electron 在渲染器中创建一个 NodeJS 运行时环境,此时新窗口中的window.open()
将按照浏览器原生的方式工作。
MacBook TouchBar 支持
针对 Mac 笔记本电脑上配置的 TouchBar 硬件,Electron 提供了一系列相关的类与操作接口:TouchBar
、TouchBarButton
、TouchBarColorPicker
、TouchBarGroup
、TouchBarLabel
、TouchBarPopover
、TouchBarScrubber
、TouchBarSegmentedControl
、TouchBarSlider
、TouchBarSpacer
。
创建应用图标
用于将 PNG 或 JPG 图片设置为托盘、Dock 和应用程序的图标。
安全原则
由于 Electron 的发布通常落后最新版本 Chromium 几周甚至几个月,因此特别需要注意如下这些安全性问题:
使用安全的协议加载外部内容
外部资源尽量使用更安全的协议加载,比如HTTP
换成HTTPS
、WS
换成WSS
、FTP
换成FTPS
等。
加载外部内容时禁用 NodeJS 集成
使用BrowserWindow
、BrowserView
、<webview>
加载远程内容时,都需要通过禁用 NodeJS 集成去限制远程代码的执行权限,避免恶意代码跨站攻击。
对于需要与远程代码共享的变量或函数,可以通过将其挂载至当前页面的
window
全局对象来实现。
渲染进程中启用上下文隔离
上下文隔离是 Electron 提供的试验特性,通过为远程加载的代码创造一个全新上下文环境,避免与主进程中的代码出现冲突或者相互污染。
处理远程内容中的会话许可
当页面尝试使用某个特性时,会弹出通知让用户手动进行确认;而默认情况下,Electron 会自动批准所有的许可请求。
不要禁用 webSecurity
在渲染进程禁用webSecurity
将导致许多重要的安全性功能被关闭,因此 Electron 默认开启。
定义 CSP 安全策略
内容安全策略 CSP 允许 Electron 通过webRequest
对指定 URL 的访问进行约束,例如允许加载https://uinika.github.io/
这个源,那么将不会被允许加载,CSP 是处理跨站脚本攻击、数据注入攻击的另外一层保护措施。
使用file://
协议打开本地文件时,可以通过元数据标签<meta>
的属性来添加 CSP 约束。
别设置 allowRunningInsecureContent 为 true
Electron 默认不允许在 HTTPS 页面中加载 HTTP 来源的代码,如果将allowRunningInsecureContent
属性设置为true
会禁用这种保护。
不要开启实验性功能
开发人员可以通过experimentalFeatures
属性启用未经严格测试的 Chromium 实验性功能,不过 Electron 官方出于稳定性和安全性考虑并不建议这样做。
不要使用 enableBlinkFeatures
Blink 是 Chromium 内置的 HTML/CSS 渲染引擎,开发者可以通过enableBlinkFeatures
启用其某些默认是禁用的特性。
禁用 webview 的 allowpopups
开启allowpopups
属性将使window.open()
创建一个新的窗口和BrowserWindows
,若非必要状况,尽量不要使用此属性。
验证 webview 选项与参数
通过渲染进程创建的<WebView>
默认不集成 NodeJS,但是它可以通过webPreferences
属性创建出一个独立的渲染进程。在<WebView>
标签开始渲染之前,Electron 将会触发一个will-attach-webview
事件,可以通过该事件防止创建具有潜在不安全选项的 Web 视图。
禁用或限制网页跳转
如果 Electron 应用程序不需要导航或只需导航至特定页面,最佳实践是将导航限制在已知范围,并禁止其它类型的导航。可以通过在will- navigation
事件处理函数中调用event.preventDefault()
并添加额外的判断来实现这一点。
禁用或限制新窗口创建
限制在 Electron 应用程序中创建额外窗口,并避免因此带来额外的安全隐患。webContents
创建新窗口时会触发一个web-contents-created
事件,该事件包含了将要打开的 URL 以及相关选项,可以在这个事件中检查窗口的创建,从而对其进行相应的限制。
Electron 2.0 版本开始,会在可执行文件名为 Electron 时会为开发者在控制台显示安全相关的警告和建议,开发人员也可以在
process.env
或window
对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS
或ELECTRON_DISABLE_SECURITY_WARNINGS
手动开启或关闭这些警告。
应用发布
Electron 的发布有别于传统桌面应用程序编译打包的发部过程,需要首先下载已经预编译完成的二进制包,Linux 下二进制包结构如下:
接下来,就可以部署前面编写的源代码,Electron 里主要有如下两种部署方式:
直接将代码放置到resources
下的子目录,比如app
目录:将应用打包加密为asar
文件以后放置到resources
目录,比如app.asar
文件。
asar 打包源码
asar是一种简单的文件扩展格式,可以将文件如同tar
格式一样前后连接在一起,支持随机读写,并使用 JSON 来保存文件信息,可以方便的读取与解析。Electron 通过它可以解决 Windows 文件路径长度的限制,提高require
语句的加载速度,并且避免源代码泄漏。
首先,需要安装asar
这个 npm 包,然后可以选择全局安装然通过命令行使用。
当然,更加工程化的方式是通过代码来执行打包操作,就像下面这样:
Electron 在 Web 页面可以通过file:
协议读取 asar 包中的文件,即将 asar 文件视为一个虚拟的文件夹来进行操作。
如果需要对 asar 文件进行 MD5 或者 SHA 完整性校验,可以对 asar 档案文件本身进行操作。
rcedit 编辑可执行文件
RcEdit是一款通过编辑窗口管理器的 rc 文件来对其进行配置的工具,Nodejs 社区提供了node-rcedit工具对 Windows 操作系统的.exe
文件进行配置,首先通过npm i rcedit --save-dev
为项目安装该依赖项。
rcedit()
函数包含有如下属性:
exePath
:需要进行修改的 Windows 可执行文件所在路径。options
:一个拥有如下属性的配置对象。version-string
:版本字符串;file-version
:文件版本;product-version
:产品版本;icon
:图标文件.ico
的路径;requested-execution-level
:需要修改的执行级别(asInvoker
、highestAvailable
、requireAdministrator
)。application-manifest
:本地清单文件的路径。callback
:函数执行完毕之后回调,完整的函数签名为function(error)
。
yarn 包管理器
Yarn 是一款由 Facebook 推出的 JavaScript 包管理器,与 NodeJS 提供的 Npm 一样使用package.json
作为包信息文件。
测试当前 Yarn 的安装版本:
初始化新项目:
添加依赖包:
将依赖项添加到不同的依赖项类别:devDependencies、peerDependencies 和 optionalDependencies。
升级依赖包:
移除依赖包:
可以直接使用yarn
命令安装项目的全部依赖:
windows-installer
windows-installer是一个用于为 Electron 应用程序构建 Windows 安装程序的 Npn 包,底层基于Squirrel(一组用于管理C#或C++开发的 Windows 应用程序的安装、更新的工具库)进行实现。
electron-build
除了像上面这样通过预编译包手动打包应用程序,也可以采用electron-forge、electron-packager等第三方包来完成这项工作,在这里笔者选择electron-builder来进行自动化打包任务。
Electron Userland 是一个维护 Electron 模块的第三方社区,electron-builder 是由其维护的一款能够同时处理 Windows、MacOS、Linux 多平台的打包编译工具。由于 electron-builder 工具包的文件体积较大,其社区强烈推荐使用更快速的yarn
来代替npm
作为包管理方案。
electron-builder 能够以命令行或者JavaScript API的方式进行使用:
(1)如果安装在项目目录下的node_modules
目录,可以直接通过 NodeJS 提供的npx
以命令行方式使用:
(2)也可以像使用其它 Npm 包那样直接调用 electron-builder 提供的 API。
官方推荐使用electron-webpack-quick-start作为 Electron 应用的项目模板。
如果项目中存在原生依赖,还需要设置nodeGypRebuild为
true
。
如果需要调试electron-builder
的行为,那么需要设置DEBUG=electron-builder
环境变量。
electron-forge
electron-forge同样是由 Electron Userland 维护的一款命令行工具,用于快速建立、打包、发布一个 Electron 应用程序。
目前 Github 上 electron-builder 的 Star 远远超过 electron-forge,由于笔者项目需要使用 React,因而也就选用带有支持 React 项目模板的 electron-builder,需要尝试 electron-forge 的同学可以移步官网查看更多信息。