如何基于 Electron 开发跨终端的应用
本文首发于政采云前端团队博客:如何基于 Electron 开发跨终端的应用
https://www.zoo.team/article/the-application-of-electron
自我介绍
首先我们分享的第一块叫端的延展。不知道大家对这张图熟不熟悉,前段时间的新闻大家应该都听到过,硅谷钢铁侠艾隆马斯克发布了第一款商业化的载人龙飞船,这张图片中就是龙飞船的控制台,知乎上有人对这张图的评价叫 JS 上天了。为什么说叫 JS 上天了呢?因为有传言说它是基于 Electron 开发的,不过这个消息并没有得到证实。但是可以证实的一点是航天飞船的触控界面 UI ,确实是基于 Chromium + JavaScript 这样的架构来实现的。这也从某种程度上说明了这种架构的一个可用性和稳定性的能力。
下面我们一起来回顾一下前端在整个端领域的发展历程。在早期,前端工程师的定义可能是基于浏览器运行环境的 Web 开发,但是随着 09 年 Node.js 的出现,让前端工程师有了脱离浏览器运行环境的开发能力。我们拥有了可以面向服务端开发的能力,前端的能力延展到了服务端。
CLI -> GUI
xxx-cli create
这样的命令去创建一个项目。创建项目完成之后,如果想进行开发,我们需要去运行 npm install
,安装所需的依赖包,最终将整个项目提交到 Git 仓库上去。这是我们新项目的创建,基于 CLI 方式的一个操作流程。GUI 赋予的价值
业务场景应用
基建场景应用
开发模式
Electron 架构
能力点
我们来介绍一下它的一些核心的能力点。
首先是 Chromium,我们可以把它理解为是一个拥有最新版浏览器特性的一个 Chrome 浏览器,它带给我们的好处就是在开发过程中无需考虑浏览器的兼容性,我们可以使用一些 ES6、ES7 最新的语法,可以放心的使用 Flex 布局,以及浏览器的最新特性,都可以尝试,不需要考虑兼容性的问题。
Node.js 则是提供了一个文件读写、本地命令调用、以及第三方扩展的能力,并且基于 Node.js 整个强大的生态,将近几十万的 Node.js 模块都可以在整个客户端内使用。
Native APIs 提供了一个统一的原生界面的能力,还包括一些系统通知、快捷键,还可以通过它来获取一些系统的硬件信息。还提供了桌面客户端的基础能力,像更新机制、崩溃报告这样的能力。
其他桌面端选型对比
Electron 提供这些能力点大大的降低了桌面端开发的成本,以及上手的门槛。当然开发桌面端的话,除了 Electron 外,还会有一些其他的选型,我们看一下它跟其他的选型相比较的话有哪些差异点。
开发桌面端首先可以选择 Native 开发,但是,在开发不同的平台的时候,需要使用不同的语言,但它的优点是具有比较好的原生体验,以及比较好的运行性能,但是它的门槛相对来说还是比较高的。
QT 是一个基于 C++ 的跨平台桌面端开发框架,它所使用的语言是使用 C++,整体性能和体验上来说,跟Native 开发的话是可以相媲美的,但由于技术栈原因,开发门槛相对来说也是比较高的。
另外两个就是 Electron 和 NW.js。这两个都是使用 Javascript 作为一个开发语言。相较于 Native 和 QT 来说,它们对前端工程师来说是相当友好的,并且它们两个有着比较相似的一个架构,都是基于 Chromium + Node.js 实现,同时它们也都有一个跨平台的支持能力。但两个的差异点是:Electron 相对来说有一个更好的一个社区的生态和社区的活跃度,我们平时如果遇到了一些问题,在社区内可能会有比较多、比较完善的解决方案,同时它对 issue 的响应速度也是比较快的。
简单 Electron 应用的结构
main
字段,通过 main
字段来定义应用的一个启动入口。我们将入口文件定义为 main.js
,在 mian.js
里我们做了哪些事情呢?首先 app 代表着整个应用,监听 app 的状态,当整个应用达到一个 ready 的状态之后,通过 Electron 提供的 BrowserWindow
,去新创建一个浏览器窗口。创建浏览器窗口之后,去加载 index.html
文件,这样的话我们就完成了一个最基础版桌面端应用的实现。基于 Electron 开发桌面端应用,和平时的开发 web 端应用有哪些不一样的,我们需要了解的两个核心概念就是:主进程和渲染进程,以及两个进程间的通信如何实现。在刚才的示例中,其中 main.js
是运行在主进程中, index.html
则是运行在渲染进程之中。下面我们通过一个简单的 Demo,来看一下如何实现两个进程之间的通信,并且如何通过主进程来进行一些 Node.js 能力调用的。进程间的通信
我们想要实现这样的效果,页面上有一个按钮,当点击按钮之后,向主进程发送了一个 say-hello
的消息,当主进程接收到消息之后,它会在系统桌面上创建一个文件叫 hello.txt
。并写入内容 Hello Mac!。
具体的我们是怎么做的?
ipcRenderer
API 向主进程发送一个叫 say-hello
这样的一个消息。当我们的主进程接收到这样一个消息之后,则可以在主进程中直接调用 Node.js 的 fs 模块,一个文件读写的模块。首先先创建一个文件,并且对这个文件写入我们所传输的内容。当文件写入成功之后,对渲染进程进行回复,通过调用 Electron 提供的 Notification
模块,显示系统通知去告知用户,这是一个简单的 Demo 的实现,其核心的点就是需要关注主进程和渲染进程的概念,以及两个进程之间是如何通过 IPC 机制进行通信的,这边是一个简单的实现。还有一些更多的应用的场景,这块就不再对 API 进行过多的介绍。工程化发展 CLI -> GUI
以我们的前端工程化平台敦煌为例,介绍一下我们是如何通过 Electron 将工程化能力由 CLI 式 变为 GUI 式的使用。首先大家先看一个视频,这个视频就是我们在最开始所提到的项目创建的整个流程的运行的演示。大家可以看到我们整个流程完成了 Git 仓库的创建、项目模板的创建、项目模板到仓库的推送,并且对 Git 项目进行本地克隆,克隆完成之后,会进行依赖的安装,并且在客户端进行重新载入和管理这样一个流程。将之前分散的单点命令操作,通过 GUI 的方式进行一个串联。这个流程只是工程化平台中的一块,我们在整个工程化平台中,实现了很多的单点命令到工作流的串联。
I2P(Install To Publish)
这边是我们整个前端应用管理平台的系统架构,大概看一下。核心流程就是上面所写到的一个 I2P 的概念,就是 install to publish
。它完成了组件、模板和项目这三个级别,从创建到发布的全流程托管。
创建阶段,主要提供了包括本地创建、Git 创建、统一的创建模板管理、创建的流程审批和创建完成的反馈。
开发阶段,提供了一个 Dev Server 的运行能力,对项目级的页面管理、依赖管理、分支管理,还有一键式的升级能力。同时还打通了 CI/CD 持续集成能力。
发布阶段,则提供了一个发布前的权限校验和合规检测、资源推送以及发布的审批机制。
数据分析,是我们整个流程中比较核心的一块,是对我们整个流程进行一些数据沉淀,并且将这些数据以可视化报表的形式进行成输出,基于这些数据将整个 I2P 的流程与其他的能力进行一个串联。
由点到线
单点命令 -> 任务流
下面我们就具体来看一下如何实现由一个单点命令到任务流这样的一个串联。将单点命令的操作变为任务流的串联模式,我们要从以下 4 个切入点来实现。
• 首先我们要将常规的一些命令调用变为函数式的调用。
• 基于这些函数式的调用,进行一个任务流的编排和组装,根据实际的开发场景,去定制一个任务流。
• 第三块我们所需要的是整个任务流的任务进度反馈机制,如何将任务执行,通过 GUI 的能力,让用户可以直观感受到整个任务的执行链路和进度。
流程的设计
npm install 变为 npm.install()
npm install
这样一个命令行的调用方式变成变为一个函数式的调用,会变为 npm.install()
这样一个调用方式。git init 变为 git.init()
将命令式执行 Promise 化
git init
这样的操作,在执行整个命令的时候,我们更多关心的是整个命令执行的结果,可能不太会关心命令执行过程中的一些输出的内容。这样的话我们就可以通过 Node.js 中的 spawn
,启动子进程来执行命令。通过监听子进程输出来判断我们整个命令的执行状态,然后对整个命令进行 Promise 封装,我们就完成了 git init
这样一个命令行调用变为 git.init()
这样一个异步的函数调用。实时输出命令执行日志
npm install
,依赖安装,或者说启动本地开发服务,整个命令的执行过程可能会比较长,我们更关注的是过程中实时的日志输出。我们怎么来做呢?首先我们这边是先创建一个 EventEmitter
实例,作为我们的日志的分发管理,同样的我们也是通过 spwan
来启动一个子进程来执行命令,并且实时的监听子进程的输出,将输出的日志通过 emitter
实例将它分发出去。当我们在主进程中拿到这样的实时日志输出之后,可以通过 Electron 主进程跟渲染进程间的 IPC 的通信,将日志实时的输出到渲染进程当中。模拟终端:反馈任务进度
上面我们提到的是主进程中对整个命令执行方式的一些改变。那么在我们的渲染进程当中,我们要怎样去实现类似于刚才视频中的终端日志反馈呢?反馈的方式有很多,我们可以通过设计一些任务的步骤条,或者进度条这样的方式来给予整个任务进度的反馈。但是更好的方式是我们可以把任务的进度,包括整个任务输出日志进行一个及时的反馈。这边我们使用的是 xterm.js。它是一个基于 ts 所编写的一个前端终端组件,可以在浏览器内实现终端应用,VsCode 也是基于 xterm.js 来实现的终端的。要如何将主进程的日志来输出到渲染进程当中,就是我们上面所提到的,在拿到一个 EventEmitter 所广播的的输出之后,要通过主进程与渲染进程之间的通信,将数据推送到渲染进程,在渲染进程所需要做的一个处理,把接受到的命令输出,实时的渲染到 xterm 实现的终端组件上面来。
更新
autoUpdater
模块,它是 Electron
内置的更新管理模块。首先需要设置 feedUrl,就是最新的更新包在更新服务端地址。当收到一个渲染进程的版本检测请求之后,调用 checkForUpdates
方法,之后,它会触发下面一系列的一些事件,我们可以通过对整个更新事件的各个生命周期的监听,来完成整个更新流程的把控。通过 Electron 内置的一个更新机制要面临的问题是更新包体积比较大。因为我们通过 Electron 所构建的桌面端的应用,它将整个 Chromium 进行了集成,就会导致即使我们写了一个很小的 Hello world 这样一个应用,它的体积压缩后也会有 40MB 左右,常规的一个应用来说可能占用 100MB 左右。这样的问题就是有一些比较小的改动的时候,就需要全量的更新,对于用户的一个体验来说并不是很好,对于这些我们有哪些解决方案?首先我们是可以对整个更新的交互设计上做一个优化。我们需要提供的是对整个更新流程的一个进度反馈,另外一点就是我们可以通过 autoUpdater,实现后台的下载。当我们完成了整个更新包的下载之后,然后再通知用户对整个应用进行一个重启,然后更新整个应用,这样的话就才从交互层面上,一定程度的避免了增量更新对用户所体验上的一些影响。当然全量更新还会存在的一个问题,如果用户量比较大的话,就会比较浪费网络资源。
增量发布
更新流程
敦煌工程化平台技术架构图
更多场景
推荐一本书
QA
“请问子洋:如何进行热更新呢?据我了解 Electron 打包出来的页面是放在包内的,如何进行在线更新?
我理解问题应该是 UI 层界面的更新。其实刚才我有提到过,我们对页面的一些静态资源是做了一个 cdn 上的托管,在更新的时候,会有一个检测更新的机制,它可以通过轮询或者服务端推送来实现,当收到静态资源版本更新的通知,通过主进程对渲染进程进行一个忽略缓存的强制刷新,或者说可以通过在主进程有相应的交互,包括升级提醒和更新日志,让用户触发页面重载,去更新 UI 层面的静态资源。
“请问子洋:Electron 和 NW.js 的区别能请您对比一下吗?
它们两个最大的区别是在于对 Node.js 和 Chromium 事件循环机制的整合的处理方式是不一样的。首先 NW.js 是通过修改源码的方式,让 Chromium 与 Node.js 的事件循环机制进行打通;Electron 实现的机制是通过启用一个新的安全线程,在 Node.js 和 Chromium 之间做事件转发,这样来实现两者的打通。这样的一个好处就是 Chromium 和 Node.js 的事件循环机制不会有这么强的耦合。另外的区别则是 NW.js 支持 xp 系统,Electron 是不支持的。相比较而言 Electron 有着更活跃的社区,以及更多的大型应用如 VS code、Atom 的实践案例,更多的区别可以参考 Electron 官方的一篇介绍:www.electronjs.org/docs/develo…
“请问子洋:更新包的文件是放在私有文件服务器还是 Gitlab 或者 Github 上面?
有比较多方式,我们的实现是通过 CDN 的托管,也可以通过 Github 或者私有文件服务器的搭建来实现。根据自己实际的业务场景和技术栈来选择。
推荐阅读