乘风破浪,遇见跨平台桌面开发平台Electron - 使用JavaScript、HTML和CSS构建,比你想象的更简单
什么是Electron
Electron(最初名为Atom Shell)是GitHub开发的一个开源框架。它允许使用Node.js(作为后端)和Chromium(作为前端)完成桌面GUI应用程序的开发。Electron现已被多个开源Web应用程序用于前端与后端的开发,著名项目包括GitHub的Atom和微软的Visual Studio Code。
Electron可以用于构建具有HTML、CSS、JavaScript的跨平台桌面应用程序,它通过将Chromium和Node.js合同一个运行的环境中来实现这一点,应用程序可以打包到Mac、Windows和Linux系统上。
Electron结合了Chromium Content Module和Node.js运行时。它允许开发人员使用网页构建图形用户界面(GUI),以及通过与操作系统无关的API访问OS X,Windows和Linux上的本机操作系统功能。
Chromium和Node本身都是广受欢迎的应用程序平台,并且都已被独立用于创建雄心勃勃的应用程序。Electron将这两个平台结合在一起,允许您使用JavaScript来构建一类全新的应用程序。
你可以在浏览器中做的任何事情,你都可以用Electron做。任何你可以用Node做的事情,你可以用Electron做的任何事情。
我们可以一起构建利用这两个平台的应用程序,并构建仅一个平台无法实现的应用程序。
起源
如果想开发一个桌面GUI应用软件,希望其能同时在Windows、Linux和Mac平台上运行,可选的技术框架并不多,在早期人们主要使用以下三个框架来完成这类工作:
- WxWidgets:https://www.wxwidgets.org
- GTK:https://www.gtk.org
- Qt:https://www.qt.io
这三个框架都是用C/C++语言开发的,受语言开发效率的限制,开发者想通过它们快速地完成桌面应用的开发工作十分困难。
近几年相继出现了针对这些框架的现代编程语言绑定库,诸如Python、C#、Go等,大部分都是开源社区提供的,但由于历史原因,要想用到这些框架的全部特性,还是需要编写C/C++代码。并且由于几乎没有高质量的Node.js的绑定库,前端程序员想通过这三个框架开发桌面应用更是难上加难。
Stack Overflow的联合创始人JeffAtwood曾经说过:"凡能用JavaScript实现的,注定会被用JavaScript实现"。桌面GUI应用也不例外,近几年两个重量级框架NW.js和Electron横空出世,给前端开发人员打开了这个领域的大门。
Electron最初由赵成和GitHub的工程师于2013年4月创建,当时名字为Atom Shell,用来服务于GitHub的开发工具Atom,2014年5月开源,2015年4月才正式更名为Electron。目前GitHub公司内部仍有一个团队在维护这个开源项目,且社区内也有很多的贡献者。
Electron更新非常频繁,平均一到两周就会有新版本发布,Issue和Pull request的回复也非常及时,一般关系到应用崩溃的问题(Crash Issue)一两天就能得到回复,普通问题一周内也会有人跟进。社区活跃程度由此可见一斑。
与Electron类似的另外一个框架是NW.js,它们都是用Web前端技术来开发桌面GUI程序。
这两个框架都与中国人有极深的渊源,2011年左右,中国英特尔开源技术中心的王文睿(Roger Wang)希望能用Node.js来操作WebKit,而创建了node-webkit项目,这就是NW.js的前身,但当时的目的并不是用来开发桌面GUI应用。
中国英特尔开源技术中心大力支持了这个项目,不仅允许王文睿分出一部分精力来做这个开源项目,还给了他招聘名额,允许他招聘其他工程师来一起完成。
2012年,故事的另一个主角赵成(Cheng Zhao)加入王文睿的小组,并对node-webkit项目做出了大量的改进。
后来赵成离开了中国英特尔开源技术中心,帮助GitHub团队尝试把node-webkit应用到Atom编辑器上,但由于当时node-webkit并不稳定,且node-webkit项目的走向也不受赵成的控制,这个尝试最终以失败告终。
但赵成和GitHub团队并没有放弃,而是着手开发另一个类似node-webkit的项目——Atom Shell,这个项目就是Electron的前身。赵成在这个项目上倾注了大量的心血,这也是这个项目后来广受欢迎的关键因素之一。再后来GitHub把这个项目开源出来,最终更名为Electron。
发展
2013年的时候,Atom编辑器问世,作为实现它的底层框架Electron也逐渐被熟知,到2014年春季被开源,那时它还是叫AtomShell。
接下来的几年,Electron在不断的更新迭代,几乎每年都有一个重大的里程碑:
- 2013年4月,Electron以Atom Shell为名起步。
- 2014年5月,Atom以及Atom Shell以MIT许可证开源。
- 2015年4月,项目被重命名为Electron。
- 2016年5月11日,电子版发布v1.0.0版本。
- 2016年5月20日,允许向Mac应用商店提交软件包。
- 2016年8月2日,支持Windows商店
- 2018年5月2号发布的2.0.0
兼容
目前支持Electron的平台有OSX、Windows、Linux:
- OSX:对于OSX系统仅有64位的二进制文档,支持的最低版本是OSX 10.8。
- Windows:仅支持Windows7及其以后的版本,之前的版本中是不能工作的。对于Windows提供x86和amd64(x64)版本的二进制文件。需要注意的是ARM版本的Windows目前尚不支持。
- Linux:预编译的ia32(i686)和x64(amd64)版本Electron二进制文件都是在Ubuntu12.04下编译的,arm版的二进制文件是在ARMv7(硬浮点ABI与DebianWheezy版本的NEON)下完成的。预编译二进制文件是否能够运行,取决于其中是否包括了编译平台链接的库,所以只有Ubuntu12.04可以保证正常工作,但是Ubuntu12.04+、Fedora21、Debian8等平台也被证实可以运行Electron的预编译版。
优劣
Electron的优点如下所示:
- 部署升级方便,用户可以通过浏览器就可以访问。
- HTML/JS/CSS编写,方便且高效。
- 可支持Windows、Linux、Mac系统。
Electron的缺点如下所示:
- 对于开发者而言:浏览器适配比较繁琐。有些应用必须指定浏览器版本(比如OCX必须是IE内核,H5必须是较高版本),必须打开浏览器,输入一长串URL地址。
- 对于用户而言:传统行业中部分用户对web应用不习惯,尤其是使用专业工具软件,大多数会觉得web应用没有桌面应用用起来踏实。
现实
Electron在2014首次开源,作为一种使用Web技术(HTML+CSS+JS)构建桌面应用程序的方式,它迅速流行起来。其设计的核心思想是将可预测的环境捆绑在一起:
- 它捆绑了自己的Chromium副本,因此,你可以确定你的HTML/CSS将如何被渲染,不必担心IE浏览器(等)随机的旧版本。
- 它捆绑了自己的Node.js副本,因此,你拥有已知的一个可移植编程平台的版本,它超越了浏览器沙箱,可以直接与本机系统交互。
这些选择在五年前很有价值,但到了2019年底,你可能会做出不同的选择。这些选择也是为什么Electron对资源极度渴求却也会闻名的关键。默认的空白Electron 8.0.0应用程序需要下载164MB(压缩后66MB),并作为4个单独的进程运行,总共消耗150MB。
案例
Electron现已被多个开源应用软件所使用,其中被广大程序员所熟知和使用的Atom、支付宝小程序IDE、Visual Studio Code编辑器就是基于Electron实现的。
Visual Studio Code
打开Visual Studio Code,前往菜单"帮助"-"切换开发人员工具"
Twitch
Microsoft Teams
Figma
VS Blazor
https://github.com/dotnet/maui/tree/main/src/BlazorWebView/src/Wpf
Blazor作为原生跨平台和Electron最大的区别就是真的是原生。以我的这个项目为例,用Blazor写的代码都是在WPF启动进程里面跑的,只有当更新html的时候才会和webview2进行通讯,而此时也不是用的tcp,而是对edge webview2进行了一层封装,所以完全没有任何网络请求。
基于此,我可以通过.NET访问系统底层的任何东西,.NET生态几乎所有的库你都可以信手拈来,Debug也浑然天成,性能当然就是跟着.NET在Windows的性能走。在UI方面,我可以用html+css轻松地渲染各种效果,如果想,也可以加入js来进行互操作。好处显而易见:.NET生态和web生态都可以比较好的融合,更小的限制,Visual Studio的支持,C#的项目还支持Hot Reload等等吧。
多进程架构
Electron继承了来自Chromium的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。
网页浏览器是个极其复杂的应用程序。除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口(或标签页)和加载第三方扩展。
在早期,浏览器通常使用单个进程来处理所有这些功能。虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。
为了解决这个问题,Chrome团队决定让每个标签页在自己的进程中渲染,从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。下方来自Chrome漫画的图表可视化了此模型:
Electron应用程序的结构非常相似。作为应用开发者,您控制着两种类型的进程:主进程和渲染器。这些类似于上面概述的Chrome自己的浏览器和其渲染器进程。
主进程
每个Electron应用都有一个单一的主进程,作为应用程序的入口点。主进程在Node.js环境中运行,这意味着它具有require
模块和使用所有Node.js API的能力。
- 窗口管理
主进程的主要目的是使用BrowserWindow
模块创建和管理应用程序窗口。
BrowserWindow
类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。您可从主进程用window
的webContent
对象与网页内容进行交互。
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')
const contents = win.webContents
console.log(contents)
注意:渲染器进程也是为
web embeds
而被创建的,例如BrowserView
模块。嵌入式网页内容也可访问webContents
对象。
由于BrowserWindow
模块是一个EventEmitter
,所以您也可以为各种用户事件(例如,最小化或最大化您的窗口)添加处理程序。
当一个BrowserWindow
实例被销毁时,与其相应的渲染器进程也会被终止。
- 应用程序生命周期
主进程还能通过Electron的app
模块来控制您应用程序的生命周期。该模块提供了一整套的事件和方法,可以使你添加自定义的应用程序行为(例如:以编程方式退出您的应用程序、修改程序坞或显示关于面板)。
这是一个实际的例子,这个app来源于快速入门指南,用app API创建了一个更原生的应用程序窗口体验。
// quitting the app when no windows are open on non-macOS platforms
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
- 原生API
为了使Electron的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的API来与用户的作业系统进行交互。Electron有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。
渲染器进程
每个Electron应用都会为每个打开的BrowserWindow
(与每个网页嵌入)生成一个单独的渲染器进程。洽如其名,渲染器负责渲染网页内容。所以实际上,运行于渲染器进程中的代码是须遵照网页标准的(至少就目前使用的Chromium而言是如此)。
因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。
虽然解释每一个网页规范超出了本指南的范围,但您最起码要知道的是:
- 以一个HTML文件作为渲染器进程的入口点。
- 使用层叠样式表(Cascading Style Sheets,CSS)对UI添加样式。
- 通过
<script>
元素可添加可执行的JavaScript代码。
此外,这也意味着渲染器无权直接访问require
或其他Node.js
API。为了在渲染器中直接包含NPM模块,您必须使用与在web开发時相同的打包工具(例如webpack或parcel)
注意:渲染器进程可以生成一个完整的Node.js环境以便于开发。在过去这是默认的,但如今此功能考虑到安全问题已经被禁用。
此刻,您也许会好奇,您在渲染器进程中的用户介面该如何与Node.js和Electron的原生桌面功能进行交互,如果这些功能都仅适用于主进程的话。而事实上,确实没有直接导入Electron內容脚本的方法。
初探
准备环境NodeJS
开始创建或者编辑Electron项目之前,至少要准备好了现代前端都依赖的NodeJS,没有的话,需要补装一个。
通过Winget补装
winget install 'OpenJS.NodeJS.LTS'
检查已安装的版本
node -v
npm -v
创建Electron应用
通其他NodeJS程序一样,Electron应用也是需要先完成NPM包初始化操作。
创建应用目录
mkdir HelloElectron
切换到应用目录
cd .\HelloElectron\
使用VSC打开项目
code .
NPM包初始化
npm init
在初始化过程中,它会问你一些问题,对Electron应用来说,应用作者(Author
)、应用描述(Description
)建议填写,另外应用入口(Entry Point
)填写main.js
,其他的就看情况来了。
创建之后,我们会得到一个项目配置文件(Package.json
),里面存放的就是我们刚刚回答的那些问题。
添加Electron包依赖
接下来,我们可以直接通过命令行,把Electron的包加到项目依赖中来。
npm install electron --save-dev --verbose
其中--save-dev
代表仅作用于当前项目,而不是全局。然后--verbose
可以显示网络请求的进度,方便查看进度。
如果网络超时的话,可能需要开启VPN,最后会得到一个成功的结果哈。
安装完成后,我们会看到项目配置文件(Package.json
)中添加了最新版本的Electron包依赖(v16.0.5
)。
{
"name": "helloelectronapp",
"version": "0.0.1",
"description": "creat first app for electron",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Taylor Shi",
"license": "MIT",
"devDependencies": {
"electron": "^16.0.5"
}
}
自定义调试
自定义调试命令
我们可以选择在项目配置文件(Package.json
)的指令脚本(scripts
)中添加关于Electron的调试启动命令。
{
"scripts": {
"start": "electron ."
}
}
创建入口主文件(main.js)
在Electron应用程序中启动入口都是从这个main.js
开始的,这里我们先在根目录建立一个空的main.js
文件即可。
Main.js控制着主进程,它运行在一个完整的Node.js环境中,负责控制您应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程。
通过命令启动调试
npm start
因为当前的入口文件main.js
是空的,所以这里不会有什么界面,但是不会报错就是成功了。
添加应用页面
在Electron中添加新页面有很多种方式,其中一种就是可以直接加载本地的HTML文件。
- 出于演示需要,我们在本地建立一个静态HTML文件(
index.html
)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</body>
</html>
- 将新页面在窗口中打开
为了把这个页面加载到当前应用窗口中来,我们需要依靠两个Electron模块,他们分别是:
app
模块,它控制应用程序的事件生命周期。BrowserWindow
模块,它创建和管理应用程序窗口。
在入口文件main.js
中,我们在头部将它们作为公共JS模块进行导入。
const { app, BrowserWindow } = require('electron')
接下来,我们构建一个创建窗口(createWindow
)方法把静态HTML文件(index.html
)加载进新的BrowserWindow
实例中。
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
然后我们通过app
模块中的ready
事件来作为调用创建窗口方法的引信。通过app.whenReady
方法我们可以监听到这个事件,在它里面再去调用上诉的创建窗口的方法即可。
app.whenReady().then(() => {
createWindow()
})
到此,完成最小演示所需的代码就完毕了,快点,我们再来运行一次。
npm start
好可爱!它如我们所预期的那样,出现了。
管理不同平台窗口生命周期
因为不同的系统平台可能存在一些应用程序窗口的差异性,那么我们需要考虑跨平台适配,通过进程(process
)的平台(platform
)属性,我们可以为指定平台提供定制化策略。
- 关闭所有的窗口之后就要推出该应用程序(Windows&Linux)
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
- 激活应用时无窗口打开就创建新窗口(MacOS)
对MacOS应用来说,应用可以在无窗口的情况下继续运行,当应用被重新激活时,如果没有窗口那么就直接创建新窗口即可。
可以通过监听激活activate
事件中补充策略,帮助在没有任何可用窗口的情况下,创建新窗口,但是创建窗口必须在Ready事件之后才行。
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
通过预加载让渲染器读取主进程数据
打包
添加Electron-Builder包
npm install electron-builder --save-dev --verbose
配置应用信息
https://www.electron.build/?msclkid=167b1f41c78e11ecb237634483f909a0
我们需要在package.json
文件里面,先完善好几个已有的字段(如果没有就添加上):
name
应用名称description
应用描述version
应用版本号author
发布者
比如:
{
"name": "helloelectronapp",
"version": "0.0.1",
"description": "creat first app for electron",
"author": "Taylor Shi"
}
配置构建信息
https://www.electron.build/configuration/configuration#configuration
接下来我们需要在package.json
文件里面新增一个构建信息的build
节点。
"build": { // 这里是electron-builder的配置
"productName":"xxxx",//项目名 这也是生成的exe文件的前缀名
"appId": "com.xxx.xxxxx",//包名
"copyright":"xxxx",//版权 信息
"directories": { // 输出文件夹
"output": "build"
},
// windows相关的配置
"win": {
"icon": "xxx/icon.ico"//图标路径
}
}
其中:
-
appId
应用ID,默认也可以不定义。如果你要定义的话,不同生态就有不同的规矩,比如Mac就按CFBundleIdentifier来、Windows就按Application User Model ID -
productName
产品名称,默认也可以不定义。如果定义了,就会优先使用它,如果没有定义就用外层的name
参数值。 -
copyright
版权所有,默认也可以不定义。
例如:
"build":{
"win":{
"icon": "Assets/HelloElectron.ico"
}
}
添加打包命令
在package.json
文件里面的scripts
节点中,增加打包指令。
"scripts": {
"pack": "electron-builder --dir",
"dist": "electron-builder"
}
例如:
"scripts": {
"start": "electron .",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},
基于Electron-Builder打包
只是为了输出二进制,使用命令:
npm run pack
最终输入得到:
总体积:185MB
输出二进制并且得到安装包,使用命令:
npm run dist
最终输入得到:
总体积:57.6MB
在整个过程中会去下载一些东西,有些是放在github上的,如果下载失败,多试几次,并且开流量代理去试验。
玩一玩安装。
安装完还自动运行。
进阶
参考
- CovalenceConf 2019: Visual Studio Code – The First Second
- Electron 简介-知乎
- 20位活跃在Github上的国内技术大牛
- Electron是什么?
- 2021年中旬了,electron到底怎么样?
- 区别于Electron的跨平台方案,Blazor
- 探索更轻量的Electron替代品来托管Blazor桌面应用程序
- C#使用WebView2替代Electron
- https://github.com/webview2/EdgeSharp
- https://github.com/chromelyapps/Chromely
- 从零搭建Electron应用 的一系列简单的 Demo
- Electron文档中心
- Electron 教程简介
- 面试官:听说你对package.json很熟?
- Electron-安装-故障排查
- Electron-快速入门
- Getting Started with Electron
- Electron multi process tool development diary 2: process management UI
- electron-quick-start
- Squirrel.Windows
- Chrome漫画
- PWABuilder-CLI-Getting Started
- 【Electron】在 WSL2 中 打包 electron Linux 版本
- Electron-builder打包详解
- 【Electron】Electron Icon 图标说明、及常见问题