从零搭建 Electron 应用
Electron 是一个优秀的跨平台桌面应用程序开源库,目前接触 Electron 的开发者也越来越多。但是笔者发现,目前社区里缺少对初学者足够友好的入门教程来帮助初学者用 Electron 搭建一个完整的开发框架。
为了解决这个问题,笔者将结合平时的一些 Electron 开发经验,渐近式的带领读者从零开始搭建一个完整的 Electron 应用。在这个教程中,笔者将使用 React 构建渲染进程。当然,读者也可以用其他框架来构建渲染进程,各种前端框架脚手架已经足够友好,所以这一点不用担心。
阅读完这篇教程,读者将会了解到:
- Electron 的核心知识点
- 如何搭建一个最简单的 Electron
- 如何将 Electron 和前端应用相结合
- 如何配置 TypeScript 以保证代码质量
- 如何跨平台打包 Electron 应用
- 如何调试 Electron
笔者将通过以下 8 个小 Demo 来介绍上面的知识点,为了保证学习质量,建议读者手把手跟着练习这些 Demo,读者可以点击这里来下载项目代码。
- 搭建一个最简单的 Electron
- 从零搭建一个 React 应用(TypeScript,Scss,热更新)
- 将 Electron 与 React 结合
- 打包 Electron 应用
- 实际开发一个小 Demo
- 主进程使用 TypeScript 构建
- 主进程监听文件变化并重启
- 在 vscode 中调试主进程和渲染进程
在开始之前,我们先聊一聊 Electron 的基础概念
Electron 基础概念
Electron 是什么?
Electron 是一个可以用 JavaScript、HTML 和 CSS 构建桌面应用程序的库。这些应用程序能打包到 Mac、Windows 和 Linux 系统上运行,也能上架到 Mac 和 Windows 的 App Store。
Electron 由什么组成?
Electron 结合了 Chromium、Node.js 以及 操作系统本地的 API(如打开文件窗口、通知、图标等)。
一些历史
-
2013 年 4 月 Atom Shell 项目启动 。
-
2014 年 5 月 Atom Shell 被开源 。
-
2015 年 4 月 Atom Shell 被重命名为 Electron 。
-
2016 年 5 月 Electron 发布了 v1.0.0 版本 。
-
2016 年 5 月 Electron 构建的应用程序可上架 Mac App Store 。
-
2016 年 8 月 Windows Store 支持 Electron 构建的应用程序 。
Electron 基础架构
Electron 与 Chromium 在架构上很相似
Chromium 运行时有一个 Browser Process
,以及一个或者多个 Renderer Process
。
Renderer Process
顾名思义负责渲染 Web 页面。Browser Process
则负责管理各个 Renderer Process
以及其他部分(比如菜单栏,收藏夹等等),如下图:
在 Electron 中,结构仍然类似,不过这里是一个 Main Process
管理多个 Renderer Process
。
而且在 Renderer Process
可以使用 Node.js
的 API,这就赋予来 Electron 极大的能力,以下是主进程以及渲染进程可以访问到的 API:
如何将 Chromium 与 Node 整合
Electron 最让人兴奋的地方在于 Chromium 与 Node 的整合。通俗的讲,我们可以在 Chromium 的控制台上做任何 Node 可以做的事。
能够做这个整合,首先得益于 Chromium 和 Node.js 都是基于 v8 引擎来执行 js 的,所以给了一种可能,他们是可以一起工作的。
但是有一个问题,Chromium 和 Node.js 的事件循环机制不同。我们知道,Node.js 是基于 libuv 的,Chromium 也有一套自己的事件循环方式,要让他们一起工作,就必须整合这两个事件循环机制。
如上图所示,Electron 采用了这样一种方式,它起了一个新的线程轮询 libuv 中的 backend fd,从而监听 Node.js 中的事件,一旦发现有新的事件发生,就会立即把它 post 到 Chromium 的事件循环中,唤醒主线程处理这个事件。
Electron 与 NW.js 的对比以及区别
和 Electron 同样出名的跨平台桌面应用开源库还有 NW.js。他们都有非常出名的应用,例如用 Electron 开发的有 vscode,用 NW.js 开发的有钉钉。
Electron
的原名叫 Atom Shell
,NW.js
的原名叫 node-webkit
;他们起初是同一个作者开发,而且这个这个作者是国人,先向大佬致敬,为我们开源这么优秀的开源工具。后来种种原因分为两个产品,一个命名为 NW.js
(英特尔公司提供技术支持)、 另一命名为 Electron
(Github 公司提供技术支持)。
两者在 GitHub 上的数据对比
|
|
可以看出 Electron
更加活跃。
两者程序的入口不同
在 NW.js
中,应用的主入口是网页或者 JS 脚本。 你需要在 package.json
中指定一个 html 或者 js 文件,一旦应用的主窗口(在 html 作为主入口点的情况下)或脚本被执行,应用就会在浏览器窗口打开。
在 Electron
中,入口是一个 JavaScript
脚本。 不同于直接提供一个 URL,你需要手动创建一个浏览器窗口,然后通过 API 加载 HTML 文件。 你还可以监听窗口事件,决定何时让应用退出。
Electron 的工作方式更像 Node.js 运行时 ,Electron 的 APIs 更加底层。
Node 集成
在 NW.js
,网页中的 Node 集成需要通过给 Chromium
打补丁来实现。但在 Electron
中,我们选择了另一种方式:通过各个平台的消息循环与 libuv
的循环集成,避免了直接在 Chromium
上做改动。这就意味着 Electron
迭代的成本更低。
准备工作
有了上面这些基础概念,接下来开始将下面 8 个 Demo 的学习。
在开始之前,我们先做一些基础的准备,
安装依赖:
|
|
为了防止意外报错,我们约定 cd
到每个 demo 里来运行相应 package.json
中的脚本。
以下的 demo 都将基于 Electron 8.0.0 版本讲解。
Demo01: 搭建一个最简单的 Electron
首先,我们会搭建一个最简单的 Electron 应用,它只有 3 个文件,点这里查看 Demo01 代码
创建 demo01
目录:
1、新建 package.json
文件
|
|
2、新建 index.html
文件
|
|
3、新建 main.js
文件
|
|
运行 yarn start
,第一个 electron 项目就轻松启动起来了。
注意 package.json
中的 main
字段,它指定了 electron 的入口文件。
在 main.js
中,我们注意到,electron
模块所提供的功能都是通过命名空间暴露出来的。 比如说: electron.app
负责管理 Electron
应用程序的生命周期, electron.BrowserWindow
类负责创建窗口。
细心的同学注意到,Demo01 其实是 Electorn 官方文档中的范例;是的,官方的范例写的非常简单友好,所以用它来作为我们一系列 Demo 的开始是非常好的选择。
Demo02: 从零搭建一个 React 应用
在 Demo02 中,我们会做一件与 Electron 无关事情 —— 从零搭建一个 React 应用。
在这个 React 应用中,我们将支持 TypeScript、Scss、热更新。
虽然说有 create-react-app
这样的的官方脚手架可以快速搭建项目,但是从零搭建可以将项目尽可能的在自己的掌控范围之内。
由于这个 Demo 与 本教程的主题无关,所以这边就不展开讲了,只展现一下 Demo 的目录结构:
|
|
对这个 Demo 感兴趣的同学可以查看Demo02 代码
以下是这个 Demo 必要的相关依赖安装:
|
|
Demo03: 将 Electron 与 React 结合
众所周知,前端项目在浏览器运行,而 Electron 是在桌面环境中运行。
在 Demo03 中,我们将尝试在 Electron 运行 React 项目。在开发环境中,Electron 将引用 React 开发环境下的 URL,以保证获得 React 热更新的能力,这也是我们在这个 Demo 中要做的事情。
在下一个 Demo 中,我们还会讲到在 Electron 打包后,Electron 将引用 React 打包后的文件,以获得更好的性能。我们先来看这个 Demo。
首先,拷贝 Demo02 文件夹,将其改名为 Demo03,并进入 Demo03:
1、将 Demo01 中的 main.js
也拷贝过来,将 main.js
中的 createWindow
修改如下:
|
|
这样 Electron 就可以加载 React 开发环境项目了。
2、在 package.json
中将 script
改成:
|
|
其中 start
启动 React 项目,start-electron
启动 Electron 项目。
3、在 webpack.config.js
中的 devServer
里添加 after
钩子函数,以便在运行 react 项目后拉起 electron 项目:
|
|
经过以上配置后,运行 yarn start
就可以同时把 React 项目 和 Electron 都启动起来了。
Demo03 详细的代码可以戳这里查看。
Demo04: 打包 Electron 应用
在上面的 Demo 中,我们简单搭建了开发环境的项目配置,但是读者的心里可能还没底,它在打包后还能正常运行吗?
有过前端开发经验的同学就会知道,很多时候,明明开发环境项目运行的很好,但是一打包之后就出问题了。不是路径引用错误就是 找不到 icon。所以,为了打消同学们的顾虑,我们将在 Demo04 中实践如何打包 Demo03 中的项目。
首先,拷贝 Demo03 文件夹,将其改名为 Demo04,并进入 Demo04:
在开始之前,笔者先简单介绍一下 Electron 主流的两款打包工具 electron-packager 和 electron-builder。
electron-builder
在社区相对更加活跃,而且笔者项目实际开发中用的也是 electron-builder
,于是我们在这个 demo 中也用 electron-builder
来打包 Electron。
1、由于打包需要当前目录有 Electron 可执行文件,所以所以首先安装 Electron
|
|
2、在 package.json
中加入 build
字段,这个字段会告诉 electron-builder
如何来打包应用。
|
|
其中,要重点关注 files
字段,它指定了打包时要包括的文件。在这个 demo 中,我们需要包括主进程的 main.js
和渲染进程需要的 React 项目打包后的 dist
文件夹。
此外,再关注一下 directories.output
字段,它表示 Eletron 打包后的输出目录,如果不配置,默认为 dist
,但是这和我们 React 项目的输出目录冲突,所以在这里我们改为 release
。
关于 electron-builder
的详细配置,干兴趣的同学可以查看文档。
3、修改 package.json
中的 scripts
字段。
3.1、修改 start-electron
命令,通过 corss-env
为其添加 ENV
环境变量:
|
|
这么做是因为 Electron 接下来要通过这个环境变量来判断此时是开发环境还是生产环境,从而做出不同的行为。
3.2、添加 build-render
命令:
|
|
它会打包 React 项目,并且在当前目录下生成 dist 输出文件。
3.3、添加 build-electron
命令:
|
|
它会读取 package.json
下 build
字段中的配置,并打包 Electron 项目,然后在当前目录下生成 release 输出文件。
命令中的 -mwl
表示打包 mac
、windows
、linux
三平台。如果读者只想打包一个平台的包,比如 Mac 版的,可以改成 -m
。
3.4、添加 build
命令:
|
|
build
命令很简单,它将安装依赖、打包 React 项目、打包 Electron 项目结合在以前,这样的话,我们只要运行 yarn build
就能成功打包 Electron 了。
4、上面提到,由于此时 demo 要兼顾开发环境和生产环境,在开发环境中,Electron 要引用 React 开发环境下的 URL,以获得 React 热更新的能力。在生产环境中,Electron 要引用 React 打包后的文件。所以,我们要对 mian.js
做一些微小的改造。
|
|
我们可以看到,此时我们根据此时的环境来加载不同的资源,开发环境加载 http://localhost:3000
,生产环境加载打包后的文件。
经过以上配置后,运行 yarn build
我们就可以打包 Electron 项目了。
Demo04 详细的代码可以戳这里查看。
Demo05: 实际开发一个小 Demo
上面的四个 Demo 中,我已经体验了从零开始 Electron 项目到成功打包一个 Electron 的完整过程。
但是我们上面做的无非是在 Eletron 中套一个可以在浏览器中跑的项目,到目前为止,我们还没有体验到 Electron 其他能力,例如:
- 调用 Node.js 的 API(如文件读写)
- 调用操作系统本地功能的 API(如打开文件窗口、通知)
在 Demo05 中,笔者将带领读者完成一个非常简单的文件读写应用,它将支持以下功能:
- 列出指定目录下的文件列表
- 支持在指定目录中添加文件
- 文件添加成功后调用系统的通知功能
此外,在这个 Demo 中,我们还会测试主进程与渲染进程之间的通信功能。
需要注意的是,Demo05 的 React 项目将无法在浏览器上运行,因为此时 React 会有很多 node 代码,而浏览器中并没有对应的 API。
首先,拷贝 Demo04 文件夹,将其改名为 Demo05,并进入 Demo05:
1、由于接下来会在我们的 React 项目中加入大量的 node 代码,比如 require('fs')
,这样的话原来 React 的 webpack 配置运行后肯定会报错,幸运的是,webpack 贴心的为我们准备了 Electron 的相关配置项。
我们可以在 webpack.config.js
中的里添加 target
字段,以表示接下来 React 的运行环境将在 Electron 的 render 进程中:
|
|
关于 webpack
的 target
字段的配置,感兴趣的同学可以阅读官方文档。
2、接下来我们将在 React 项目中添加一个组件,用它来查看文件列表并添加新的文件:
在 Demo05 中新建 src/container/file-list
目录,并添加 index.tsx 文件:
|
|
上面的组件比较简单,我们可以看到,在选择文件夹时,会调用 remote.dialog.showOpenDialog
, 它会打开系统的文件窗口。然后,我们可以用 node 的 fs
模块来写入或者读取文件。在读取成功后,我们还可以通过remote
的 Notification
来调用系统的通知功能。
总的来说,在前端项目中调用 node 相关的模块,体验很奇妙。
3、通过 ipcMain
和 ipcRenderer
我们可以实现渲染进程与主进程之间的通信。
ipcMain 在主进程中使用,用来处理渲染进程(网页)发送的同步和异步的信息:
|
|
ipcRenderer 在渲染进程中使用,用来发送同步或异步的信息给主进程,也可以用来接收主进程的回复信息。
|
|
当然,我们还可以在 Render 进程中直接使用 remote 模块, 这样的话就可以直接调用 main 进程对象的方法, 而不必显式发送进程间消息。
|
|
经过以上改造,运行 yarn start
我们就可以体验真正意义上的桌面应用了。
Demo05 详细的代码可以戳这里查看。
Demo06: 在主进程中使用 Typescript
在之前的 Demo 中,我们会发现,在渲染进程中,我们已经用上来 TypeSctipt。但是在主进程中,用的依旧是 javascript。考虑将来项目会越来越大,为了保证项目的可靠性,在这个 demo 中,我们会将主进程也改造成 Typescipt。
首先,拷贝 Demo05 文件夹,将其改名为 Demo06,并进入 Demo06:
1、新建 webpack.main.config.js
文件,之后我们会用这个文件的 wabpack 配置来打包主进程的代码,配置如下:
|
|
这是一段很简单的 webpack 配置,其中主要注意两点:
1.1、需要将 target
配置为 electron-main
表示以接下来 打包的代码将在 Electron 的 mian 进程中执行
1.2、由于 webpack 会对 __dirname
和 __filename
做其他额外的处理,为了保证 __dirname
、__filename
的行为和在 node 中保持一致,添加如下配置:
|
|
如果不这样配置,打包后 __dirname
和 __filename
将都是 /
;
2、将 webpack.config.js
改名为 webpack.renderer.config.js
,用来和 webpack.main.config.js
保持对应。
3、修改 package.json
中的 script
字段:
|
|
其中注意两点:
3.1、start-electron
将在运行 electron .
先打包主进程的 typescrip 代码。
3.2、build
执行后将先打包渲染进程,再打包主进程,最后再打包整个 Electron 应用。
经过以上改造,主进程也改造成 typescript 了,项目的可靠性大大增强。
Demo06 详细的代码可以戳这里查看。
经过以上 6 个 demo 的学习,同学们已经有能力搭建一个完整的 Electron 应用。但是我们还有一些进阶的用法,感兴趣的同学可以继续往下阅读。
Demo07: 主进程监听文件变化并重启
前面的开发中我们发现,渲染进程修改后可以自动刷新,而主进程却不行,这可能会影响到我们的开发效率。
所以,在 Demo07 中,通过 nodemon
,我们将主进程也改造成修改后可以自动刷新。
首先,拷贝 Demo06 文件夹,将其改名为 Demo07,并进入 Demo07:
1、安装 nodemon
:
|
|
2、在 package.json
添加一行脚本:
|
|
我们之后会通过 nodemon
来启动 Electron,它将监听 main.ts
的文件变化。如果发生变化,则会从新运行 start-electron
命令。
3、将 webpack.renderer.config.js
中 devServer
的 after 钩子中的 start-electron
改为 start-electron-with-nodemon
:
|
|
经过以上的简单配置,我们的主进程也能修改文件后自动刷新了。
Demo07 详细的代码可以戳这里查看。
Demo08: 在 vscode 中调试主进程和渲染进程
任何时候,如果代码出了 bug,我们的第一反应是打印 log 看一下出了什么问题。但是这样的调试方式相对低效,有些时候,我们需要借用编辑器的调试功能来帮助我们调试代码。
在这个 Demo 中,笔者将尝试用 vscode 自带的调试工具来调试 electron 的主进程和渲染进程;
由于 vscode 中的调试配置项相对较多,所以强烈建议大家先看一遍 vscode 调试的官方文档,或者看一下 github 上 Electron 调试实际案例
本文的配置与上面提到的实际案例有一些差异,因为我们 demo 开发环境的 render 进程是用一个 web server 启动的。
首先,拷贝 Demo07 文件夹,将其改名为 Demo08,并进入 Demo08:
1、在 webpack.main.config.js
和 webpack.render.config.js
中添加 devtool: 'source-map'
。因为主进程和渲染进程都是用 typescript
写的,需要在打包时生成 source maps
以形成映射,才能在 typescript
文件中正确的调试代码。详情可以查看官方文档
2、改造webpack.renderer.config.js
,将 devServer
的 after 钩子函数。
|
|
这也就意味着运行渲染进程后,只会将主进程的代码打包一下,而不再将主进程启动起来。因为在稍后的调试中我们会在 vscode 的 launch.json
中启动主进程。
3、运行yarn start
,启动渲染进程,并且给主进程打包。
4、在当前项目目录下创建一个 .vscode
目录,并且在该目录下创建一个 launch.json
文件,在该文件里添加如下配置:
|
|
我们可以看到,在 configurations
中有两个对象:
其中 “Electron: Main” 表示的是对主进程的调试,它的 request
为 launch
,表示在调试的时候启动主进程。我们还可以通过 env
来传入环境变量。它还会暴露一个 --remote-debugging-port
的远程调试端口,这个端口很重要,因为在接下来调试渲染进程时会用到它。
“Electron: Renderer” 表示的是对渲染进程的调试,它的 request
为 attach
,表示只是连接到正在调试的进程。而它的 port
刚好是 --remote-debugging-port
暴露出来的端口,url
则是本地渲染进程的地址。
接着,我们会看到 compounds
,它的作用很简单,就是把主进程调试和渲染进程调试结合起来。当调试时选择 Electron: All
时,就可以把 “Electron: Main” 和 “Electron: Renderer” 都拉起来。
这一块的配置比较多,其中每个配置的作用可以参考 vscode 的官方文档。
5、点击 vscode 自带的调试按钮,选择 Electron: All,就可以将 electron 启动起来了,这时候在主进程的 .ts
文件和渲染进程的 .ts
文件中打断点就可以发现都能起作用了。
注意,实际测试发现,主进程的调试需要在打包后的
main.js
中打一次断点,然后在通过source map
和 js 文件生成的只读的 typescipt 文件中打断点才能顺利调试。
经过以上的配置,我们可以顺利的在主进程和渲染进程中调试代码了。
Demo08 详细的代码可以戳这里查看。
总结
通过以上的一系列 Demo,我们重零搭建一个完整的 Electron 应用。
我们了解了 Electron 的核心知识点、搭建一个最简单的 Electron,将 Electron 和前端应用相结合,配置 TypeScript 以保证代码质量,跨平台打包 Electron 应用以及 如何调试 Electron。
这些 demo 的完整代码可以点这里查看,如果感觉这些 demo 写的不错,可以给笔者一个 star,谢谢大家阅读。