Skip to content

简介

由来

如果想开发一个桌面 GUI 应用软件,希望其能同时在 Windows、Linux 和 Mac 平台上运行,可选的技术框架并不多,在早期人们主要用 wsWidgets、GTK 或 Qt 来做这类工作。
这三个框架都是用 C/C++ 语言开发的,受语言开发效率的限制,开发者想通过它们快速地完成桌面应用的开发工作十分困难。

近几年相继出现了针对这些框架的现代编程语言绑定库,诸如 Python、C#、Go 等,大部分都是开源社区提供的,但由于历史原因,要想用到这些框架的全部特性,还是需要编写 C/C++ 代码。
并且由于几乎没有高质量的 Node.js 的绑定库,前端程序员想通过这三个框架开发桌面应用更是难上加难

Stack Overflow 的联合创始人 Jeff Atwood 曾经说过,凡能用 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)一两天就能得到回复,普通问题一周内也会有人跟进。
社区活跃度由此可见一斑

简介

https://electronjs.org/

https://github.com/electron/electron

https://github.com/sindresorhus/awesome-electron

文档:

https://www.electronjs.org/docs

稳定版本:

https://www.electronjs.org/releases/stable

使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用

Web 技术 - Electron 基于 Chromium 和 Node.js
开源-众多贡献者组成的活跃社区共同维护的开源项目
跨平台

在 Electron 本身迅猛发展的同时,其社区生态也呈爆发式增长,兼之 Electron 可以复用 Web 和 Node.js 生态内的组件,这使得开发者在组件选型时经常会犯错。
比如,我就不推荐在 Electron 应用内使用 jQuery、axios 和 electron-vue 等组件。
这是新手面临的一个困难。

Electron 的生态

https://github.com/electron-userland/electron-builder

electron-builder 是一个 Electron 的构建工具,它提供了自动下载、自动构建、自动打包、自动升级等能力,是 Electron 生态中的基础支持工具,大部分流行的 Electron 应用都使用它进行构建和分发

在 Electron 应用内存取本地数据,可以使用 Cookie、LocalStorage 或 IndexedDB 这些传统的前端技术,也可以选择 Electron 生态内的一些方案,例如 rxdb 是一个可以在 Electron 应用内使用的实时 NoSQL 数据库;如果希望使用传统的数据库,也可以在 Electron 内使用 SQLite 数据库

https://github.com/nklayman/vue-cli-plugin-electron-builder

https://github.com/SimulatedGREG/electron-vue

Vue CLI Plugin Electron Builder 和 electron-vue 是两个非常不错的工具,开发者可以基于它们轻松地在 Electron 应用内使用 Vue 及其组件(包括 HMR 热更新技术)。
虽然后者拥有更多的 GitHub star,更受欢迎,但推荐使用前者。
前者基于 Vue CLI Plugin 开发,更新频繁,而后者已经有近一年时间没更新过了。

electron-react-boilerplate 是一个项目模板,它把 Electron、React、Redux、React Router、Webpack 和 React Hot Loader 组合在一起。
开发者基于此模板可以快速构建 React 技术体系的 Electron 应用。

angular-electron 也是一个项目模板,开发者可以基于它快速构建基于 Angular 和 Electron 的应用

如果不希望使用上述前端框架,仅希望使用 webpack 于传统 Web 前端开发技术开发 Electron 应用,可以考虑使用 electron-webpack 嘴贱完成工作

不足

基于 Electron 开发桌面 GUI 应用并不是完美的方案,它也有它的不足,综合来说有以下几点

打包后的应用体积巨大

一个功能不算多的桌面应用,通过 electron-builder 压缩打包后至少也要 40MB。
如果开发者不做额外的 Hack 工作的话,用户每次升级应用程序,还要再下载一次同样体积的安装包,这对于应用分发来说是一个不小的负担。
但随着网络环境越来越好,用户磁盘的容积越来越大,此问题给用户带来的损失会慢慢被削弱。

开发复杂度较大,进阶曲线较陡

跨进程通信是基于 Electron 开发应用必须要了解的知识点,虽然 Electron 为渲染进程提供了 remote 模块来方便开发人员实现跨进程通信,但这也带来了很多问题,比如某个回调函数为什么没起作用、主进程为什么报了一连串的错误等,这往往给入门但需要进阶的开发者带来困惑。

版本发布过快

为了跟上 Chromium 的版本发布节奏,Electron 也有非常频繁的版本发布机制,每次 chromium 改动,都可能导致 Electron 出现很多问题,甚至稳定版本都有很多未解决的问题。
幸好 Electron 的关键核心功能一直以来都是稳定的

安全性问题

Electron 把一些有安全隐患的模块和 API 都设置为默认不可用的状态,但这些模块和 API 都是非常常用的,因此有时开发者不得不打开这些开关。
但是,一旦处理不当,就可能导致开发的应用存在安全隐患,给开发者乃至终端用户带来伤害。
安全问题有很多值得关注的技术细节,以至于 Electron 官方文档中专门开辟出来一个章节号召程序员重视安全问题。
但我认为,很多时候安全和自由是相悖的,在不损失自由的前提下提升安全指标的工作是值得肯定的,如果哪天 Electron 以安全为由停用脚本注入的技术,相信很多开发者都会反对。

资源消耗较大

Electron 底层基于的 Chromium 浏览器一直以来都因资源占用较多被人诟病,目前来看这个问题还没有很好的解决办法,只能以来 Chromium 团队的优化工作

除了以上这些问题外,Electron 还不支持老版本的 Windows 操作系统,比如 Windows XP。
在中国还有一些用户是使用 Windows XP 的,开发者如果需要面向这些用户,应该考虑使用其他技术方案

快速启动

1
2
3
4
5
6
# 克隆示例项目的仓库
$ git clone https://github.com/electron/electron-quick-start
# 进入这个仓库
$ cd electron-quick-start
# 安装依赖并运行
$ npm install && npm start

npm install遇到问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> electron@7.0.0 postinstall /Users/nocilantro/electron-quick-start/node_modules/electron
> node install.js

(node:8874) UnhandledPromiseRejectionWarning: RequestError: connect ECONNREFUSED 0.0.0.0:443
    at ClientRequest.<anonymous> (/Users/nocilantro/electron-quick-start/node_modules/got/source/request-as-event-emitter.js:178:14)
    at Object.onceWrapper (events.js:288:20)
    at ClientRequest.emit (events.js:205:15)
    at ClientRequest.origin.emit (/Users/nocilantro/electron-quick-start/node_modules/@szmarczak/http-timer/source/index.js:37:11)
    at TLSSocket.socketErrorListener (_http_client.js:402:9)
    at TLSSocket.emit (events.js:200:13)
    at emitErrorNT (internal/streams/destroy.js:91:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
    at processTicksAndRejections (internal/process/task_queues.js:84:9)
(node:8874) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:8874) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
added 1 package from 1 contributor in 1.788s

查看了~/.npmrc:

1
registry=https://registry.npm.taobao.org

解决方法:
npm config set proxy null

此时的.npmrc:

1
2
registry=https://registry.npm.taobao.org/
proxy=null

设置完之后删除node_modules/electron文件夹,然后重新npm install

npm install 出现问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
npm ERR! code E404
npm ERR! 404 Not Found - GET http://registry.npm.taobao.org/@types/node/-/node-14.17.4.tgz - [not_found] document not found
npm ERR! 404 
npm ERR! 404  '@types/node@http://registry.npm.taobao.org/@types/node/-/node-14.17.4.tgz' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404 
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/nocilantro/.npm/_logs/2021-06-30T08_10_40_811Z-debug.log

解决办法:执行 npm config set registry http://registry.npmjs.org,然后再重新 npm install

搭建开发环境

yarn 由 Facebook 的工程师开发,相对于 Node.js 自带的 npm 包管理工具来说,它具有速度更快、使用更简捷、操作更安全的特点,建议安装使用,安装命令如下:

1
2
3
4
npm install -g yarn

yarn --version
1.22.5

接下来,我们创建第一个 Electron 应用,先新建一个目录chapter1,在此目录下打开命令行,执行如下命令创建一个 Node.js 项目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ yarn init
yarn init v1.22.5
question name (chapter1):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
✨  Done in 5.18s.

该命令执行完成后,会有一系列提示,要求用户输入项目名称、项目版本、作者等信息。
这里我们全部采用默认设置,直接按回车键即可

项目创建完成之后,该目录下会生成一个 package.json 文件,此文件为该项目的配置文件,内容如下:

1
2
3
4
5
6
{
  "name": "chapter1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

在编写具体的项目代码之前,需要先安装 Electron 依赖包,它大概有 50 到 70 MB(实际上是一个精简版的 Chromium 浏览器),而且默认是从 GitHub 下载的,下载地址是https://github.com/electron/electron/releases(文件实际上存放在 Amazon 的 AWS 服务器上,须经由 GitHub 域名跳转)。
对于中国用户来说,下载速度很慢,大部分时候无法安装成功。

好在阿里巴巴的工程师在国内搭建了 Electron 的镜像网站: https://npm.taobao.org/mirrors/electron(注意,此地址与下方命令行中的地址不同)。
我们可通过如下指令配置 Electron 的镜像网站:

1
2
3
4
$ yarn config set ELECTRON_MIRROR https://cdn.npm.taobao.org/dist/electron
yarn config v1.22.5
success Set "ELECTRON_MIRROR" to "https://cdn.npm.taobao.org/dist/electron".
✨  Done in 0.06s.

环境变量设置好之后,再在命令行执行如下命令,以安装 Electron:

1
yarn add electron --dev

--dev声明安装的 electron 模块只用于开发。

稍微等待一会,electron 模块就安装好了,安装完成后,package.json 增加了如下配置

https://www.electronjs.org/docs/tutorial/installation#mirror

~/.zshrc中添加export ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/

然后执行source ~/.zshrc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
https://npm.taobao.org/mirrors/electron/7.1.14/

ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/"

npm config set ELECTRON_MIRROR http://npm.taobao.org/mirrors/electron/


npm config set electron_mirror https://cdn.npm.taobao.org/dist/electron/
npm config set registry http://registry.npm.taobao.org/


npm install electron --save-dev
cd node_modules/electron
node install.js
1
2
3
4
5
6
7
8
9
{
  "name": "chapter1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "electron": "^12.0.0"
  }
}

一个 Node.js 模块的版本号一般会包含三个数字,其中第一个数字代表主版本号,第二个数字代表此版本号,第三个数字代表修订版本号。

Electron 版本号前面还有一个^符号,此符号的意义为: 安装此依赖库时允许次版本号和修订版本号的提升,但不允许主版本号提升。
举例来说,如果package.json里的记录的是^1.1.1版本号,那么安装依赖包后,可能安装的是1.5.8版本,但不会是 2.x.y 版本。

另外,如果版本号前面的符号不是^而是~,这种情况下则只允许修订版本号提升,主版本号和次版本号均不允许提升

如果主版本号为 0 或主版本号、次版本号为 0,以上规则则应另当别论

安装成功后,项目目录下还增加了 node_modules 子目录,该目录下存放着项目运行时依赖的 Node.js 包。
Electron 依赖包也包含其中。
Electron 依赖包是一个普通的 Node.js 包,它导出了 Electron 的安装路径(安装路径指向 Electron 依赖包所在目录的 dist 子目录)

Node.js 有三种模块。第一种是核心模块,其存在于 Node.js 环境内,比如 fs 或 net 等

第二种是项目模块,其存在于当前项目中,一般都是项目开发者手动提供的。require 这类模块,一般以./path/fileName这种相对路径寻址

第三种是第三方模块,这种模块一般都是项目开发者通过 yarn 或 npm 工具手动安装到项目内的。
require 此类模块一般传入模块名即可,Node.js 环境会为我们到当前 node_modules 目录下寻找模块。

此处,我们安装的就是一个第三方模块

为了使用 Electron 依赖包,我们需要在package.json中增加了一个 scripts 配置节,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "name": "chapter1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "electron ./index.js"
  },
  "devDependencies": {
    "electron": "^12.0.0"
  }
}

scripts 配置节允许我们为当前项目设置一组自定义脚本,这里 start 脚本代表我们要使用 Electron 启动本项目。
待业务代码开发完成后,只要在命令行输入以下启动命令,即可运行程序:

1
npm run start

创建窗口界面

虽然搭建了项目环境,但此时项目内还没有业务代码,因此还是不能运行。
新建index.html文件并输入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>窗口标题</title>
</head>
<body>
  <div style="padding: 60px; font-size: 38px; font-weight: bold; text-align: center;">
    Hello World
  </div>
</body>
</html>

这是一个简单的网页,在屏幕上显示了 Hello World 字样。
网页的 title 被设置为"窗口标题",后面你会发现网页的 title 会被 Electron 设置为窗口的标题

启动窗口

接下来,我们在项目根目录下新建 index.js 文件,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var electron = require('electron')
var app = electron.app
var BrowserWindow = electron.BrowserWindow
var win = null
app.on('ready', function() {
  win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true
    }
  })
  win.loadFile('index.html')
  win.on('closed', function() {
    win = null
  })
})
app.on('window-all-closed', function() {
  app.quit()
})

上述代码中,有些地方如果用 ES6 语言实现会更加简洁,但是作者没有这么做,目的是想使学习难度曲线更平缓

我们知道 Electron 内部集成了 Node.js,所以上述代码第一行使用 Node.js 的 require 指令加载了 Electron 模块。
本例中只用了 Electron 模块的 app 对象和 BrowserWindow 类型

扩展

长久以来 JavaScript 都不支持模块化,但随着前端工程越来越庞大、复杂,模块化的需求也越来越高。
为此,社区推出了多种模块化的实现和规范,比如 AMD 规范、CMD 规范和 CommonJs 规范等。
Node.js 使用的是 CommonJs 规范,它通过 module.exports 导出模块,通过 require 倒入模块。演示代码如下:

1
2
3
4
5
6
7
// 模块文件: module.js
module.exports = {
  title: 'My name is module',
  say: function() {
    console.log('log from module.js')
  }
}
1
2
3
4
// 入口文件: main.js
var myModule = require('./module')
myModule.say()
console.log(myModule.title)

把以上两个文件放到同一个目录下,在该目录下打开命令行,在命令行执行如下指令

1
node main

最终程序输出:

1
2
log fron module.js
My name is module

这说明入口程序已经加载了模块 module.js,并且能访问此模块下导出的内容。这就是 Node.js 为开发者提供的模块机制

一旦一个模块被导入到运行环境中,就会被缓存。
当再次尝试导入这个模块时,就会读取缓存中的内容,而不会重新加载一遍这个模块的代码。
这种机制不仅避免了重复导入相同模块冲突的问题,还保证了程序的执行效率

app 代表着整个应用,通过它可以获取应用程序生命周期中的各个事件。
我们在 app 的 ready 事件中创建了窗口,并且把窗口对象交给了一个全局引用,这样做是为了不让 JavaScript 执行引擎在垃圾回收时回收这个窗口对象。

在创建窗口时,我们传入了配置对象

1
2
3
webPreferences: {
  nodeIntegration: true
}

此配置对象告知 Electron 需要为页面集成 Node.js 环境,并赋予 index.html 页面中的 JavaScript 访问 Node.js 环境的能力

窗口创建完成后,我们让窗口加载了 index.html

重点

如果加载的页面是一个互联网页面,你无法验证该页面提供的内容是否可靠,应该关闭这个选项:

1
2
3
webPreferences: {
  nodeIntegration: false
}

否则这个页面上的一些恶意脚本也会拥有访问 Node.js 环境的能力,可能会给你的用户造成损害

在窗口关闭时,我们把 win 这个全局引用置空了。
所有窗口关闭时,我们退出了 app,由于在本应用中其实只有一个窗口,所以 app 退出的代码完成可以写在窗口关闭的事件中。

现在在命令行运行npm run start,窗口成功启动了。
窗口的标题即为网页的 title,窗口内容即为网页 body 内的元素

这是一个非常简陋的窗口,但不要着急

引用 JavaScript

Electron 给 index.js 提供了完整的 Node.js 环境的访问能力,index.js 可以像所有 Node.js 程序一样,通过 require 引入其他 js 文件

在 index.html 中,我们完全可以像正常的网页一样引入 js 文件,新建一个 objRender.js 文件,代码如下:

1
window.objRender = {key: 'value'}

在 body 标签的末尾,增加如下代码:

1
2
3
4
<script src="./objRender.js"></script>
<script>
  alert(window.objRender.key)
</script>

运行程序,会弹出相应的对话框,说明 objRender.js 已经被引入到 index.html 中

由于我们在创建窗口时,允许页面访问 Node.js 的环境,所以在 index.html 内也可以通过 require 的方式访问其他 js 文件。
我们先新建一个 ObjRender2.js 文件,代码如下:

1
2
3
module.exports = {
  key: 'value'
}

再在 index.html 的 script 标签内增加如下两行代码:

1
2
let ObjRender2 = require('./ObjRender2')
alert(ObjRender2.key)

修改index.jswebPreferences配置:

1
2
3
4
webPreferences: {
  nodeIntegration: true,
  contextIsolation: false
}

程序依然正常运行,并且弹出两个 ALert 对话框,内容都是 value。

Electron API 演示工具

Electron 有非常丰富的 API。
虽然 Electron 团队在官方文档上做了大量的讲解,但文档描述对于使用一些复杂 API 来说颇有局限性,
所以 Electron 团队又推出了 Electron API 演示工具 Electron API Demos

https://github.com/electron/electron-api-demos/releases

试验工具 Electron Fiddle

如果你只是想验证一段简短的代码是否可以在 Electron 框架内正常运行,那么以之前介绍的方式创建 Electron 项目就略显复杂了。
Electron 官方团队为开发者提供了一个试验工具

Electron Fiddle(https://github.com/electron/fiddle/releases)

简单项目示例

https://github.com/hokein/electron-sample-apps

https://github.com/electron/electron-api-demos

https://github.com/electron/simple-samples

https://github.com/electron/electron-quick-start

相关项目

https://github.com/felixrieseberg/windows95

数据库

https://github.com/typicode/lowdb

https://github.com/Level/level