作者简介
隋丰蔚,携程无线平台研发部前端工程师,现负责开发者工具NFES Developer Tools的设计与研发。
引言
目前,主流的桌面应用开发方法有几种,一是使用纯Native技术栈进行开发,比如说Windows上使用C++,Mac上使用Objective-C。这种方式能够实现最好的性能,但是开发成本比较高,周期也长,而且需要分别开发Windows和Mac版本,人员投入比较大。
二是基于Qt等Native框架进行开发,这种方案可以获得接近Native的性能体验,但是学习成本仍然较高,而且界面开发效率不高,没有办法满足快速迭代的需求。
第三种则是以Electron为代表的,允许我们使用web技术开发桌面应用的框架。这种方案背后是整个web技术体系,资源丰富,跨平台性好,开发效率高,甚至于我们可以使用一套代码逻辑同时开发桌面应用和web应用,特别适合企业用来开发一些偏业务型的桌面应用。而且,Electron由GitHub维护,社区活跃度高,像我们熟悉的VS Code,Skype都是基于Electron构建的。
因此,如果不是要开发对性能要求很高的桌面应用,团队中web开发人员又相对充足,Electron是一个比较合适的选择。
本文将介绍Electron、开发过程中可能会遇到的问题和场景,以及Electron在DevTools中的实践,希望可以为想要开发Electron应用的小伙伴们提供一点参考或者思路。
一、初识Electron
根据官方文档,基于Electron,我们可以使用JavaScript,HTML 和 CSS构建跨平台的桌面应用。目前Electron支持的平台有Mac, Windows 和 Linux。
1.1 Quick Start
先来看一下如何使用Electron快速构建一个桌面应用,目录结构如下图所示:
其中,index.html就是我们平时开发的web页面,负责界面展示。main.js则是整个Electron应用的入口文件,如下:
main.js中首先引入了app和BrowserWindow模块,app模块主要负责应用级别的事情,包括应用的生命周期。可以看到,Electron初始化完成之后,会触发app ready,这时就可以开始做自己的事情了,比如说在这里我们创建了一个窗口,然后加载了index.html。如此,一个Electron应用就可以运行了,Easy Peasy。
1.2 Electron工作机制
之所以可以使用web技术构建桌面应用,其实是因为Electron做了一个整合,它集成了Chromium和Node.js,同时提供了一系列可以操作原生GUI的API。
而能够做这个整合,首先得益于Chromium和Node.js都是基于v8引擎来执行js的,所以给了一种可能,他们是可以一起工作的。
但是有一个问题,Chromium和Node.js的事件循环机制不同。我们知道,Node.js是基于libuv的,Chromium也有一套自己的事件循环方式,要让他们一起工作,就必须整合这两个事件循环机制。
如上图所示,Electron采用了这样一种方式,它起了一个新的线程轮询libuv中的backend fd,从而监听Node.js中的事件,一旦发现有新的事件发生,就会立即把它post到Chromium的事件循环中,唤醒主线程处理这个事件。
1.3 Electron应用架构
Chromium是多进程模式,每个Tab都是一个独立的进程在运行,从而确保了它的稳定性。Electron延续了多进程的模式,每个窗口对应一个独立的渲染进程,里面运行的就是web页面。渲染进程统一由主进程管理,如下图所示。
1.4 通信
在Electron中,应用级别的活动以及原生GUI模块只能在主进程中运行,渲染进程则主要负责界面展示。这时候就需要解决主进程和渲染进程之间的通信问题。
IPC(Inter-Process Communication)
Electron提供了两个模块,ipcMain和ipcRenderer,它们都是Node.js EventEmitter的实例,使用哪个取决于所在的进程。
主进程:
渲染进程:
remote模块
remote模块允许在渲染进程中直接调用主进程的模块和方法。从底层实现的角度,remote其实是对ipc做了一层封装,它除了能帮我们避免繁琐的ipc消息传递,remote和ipc还有一个本质的区别。
来看一个具体的例子,如下图所示,主进程的global上挂了一个globalData对象,现在想在渲染进程中获取这个对象中test属性的值。
主进程:
渲染进程:
来看一下remote是怎么取值的。如下图所示,首先,渲染进程通过ipc给主进程发消息说需要global上的globalData对象,主进程收到消息后,取到相应的对象,处理之后就把globalData相关的信息传回到渲染进程。渲染进程拿到对象之后,直接重写了它的get和set方法。因此,这时候再获取test值的时候,渲染进程会再次发送消息到主进程来获取。
基于这样的机制,可以看出,虽然是在两个进程中,但是完全可以把remote取回的对象当作是对主进程中这个对象的引用,因为我们获取到的值总是和主进程中的一致,而使用ipc通信,其实是对数据进行了序列化和反序列化,渲染进程拿到的对象和主进程已经没什么关系了。
二、探索Electron
到这里,大家对Electron已经有一个基本的印象了。下面来看Electron开发过程中可能会遇到的几个问题和场景。
2.1 启动时间优化
Electron应用创建窗口之后,由于需要初始化窗口,加载html,js以及各种依赖,会出现一个短暂的白屏。除了传统的,比如说延迟js加载等web性能优化的方法,在Electron中还可以使用一种方式,就是在close窗口之前缓存index页面,下次再打开窗口的时候直接加载缓存好的页面,这样就会提前页面渲染的时间,缩短白屏时间。
但是,优化之后也还是会有白屏出现,对于这段时间可以用一个比较tricky的方法,就是让窗口监听ready-to-show事件,等到页面完成首次绘制后,再显示窗口。这样,虽然延迟了窗口显示时间,总归不会有白屏出现了。
2.2 CPU密集型任务处理
对于cpu密集型或者long-running的task,我们肯定不希望它们阻塞主进程或者影响渲染进程页面的渲染,这时候就需要在其他进程中执行这些任务。通常有三种方式:
2.3 数据持久化存储
为了使应用在offline的情况下也可以正常运行,对于桌面应用,我们会将一些数据存储到本地,常见方式有:
2.4 安全性考虑
在Electron应用中,web页面是可以直接调用Node.js api的,这样就可以做很多事情,比如说操作文件系统,但同时也会带来安全隐患,建议大家渲染进程中禁用NodeJS集成。
如果需要在页面中使用node或者electron的api,可以通过提前加载一个preload.js作为bridge,这个js会在所有页面js运行前被执行。我们可以在里面做很多事情,比如说把需要的node方法放到global或者window中,这样页面中就没办法直接使用node模块,但是又可以使用需要的某些功能,如下图所示。
除此之外,还要注意,使用安全的协议,比如说https加载外部资源。在Electron应用中,可以通过监听新窗口创建和页面跳转事件,判断是否是安全跳转,加以限制。亦可以通过设置CSP,对指定URL的访问进行约束。
2.5 应用体积优化
对于Electron应用打包,首先会使用webpack分别对主进程和渲染进程代码进行处理优化,和web应用一样。有点区别的地方是配置中主进程的target是electron-main, 渲染进程的target是electron-renderer。除此之外,还要对node做一些配置,我们是不需要webpack来polyfill或者mocknode的全局变量和模块的,所以设为false。
之后,在基于electron-builder将应用build成不同平台的安装包,需要注意的是,对于package.json,尽可能地把可以打包到bundle的依赖模块,从dependencies移到devDependencies,因为所有dependencies中的模块都会被打到安装包中,会严重增大安装包体积。
三、Electron在NFES DevTools中的实践
最后,分享一下Electron在NFES DevTools中的应用。
NFES是携程新推出的一整套无线前端解决方案,适用于pc,h5,hybrid多个场景,同时支持服务端渲染和单页模式。
NFES DevTools作为辅助开发平台,能够为开发人员提供稳定的,不受本地Node.js版本、全局模块等外部依赖干扰的开发环境,以及NFES项目相关的构建,调试,发布,性能监控等功能,从而帮助开发人员更好的开发和维护NFES应用。
先从整体应用架构的角度看一下NFES DevTools。如上图所示,NFES DevTools是基于Electron构建的。
主进程主要做了这几件事情,首先是app级别的活动,包括生命周期,自动更新;然后提供了一系列的remote service,比如说窗口管理,应用级别数据管理,cookie管理,让渲染进程可以直接调用。其次就是Native GUI相关的活动,像创建原生菜单,托盘图标,通知提醒等。最后,在窗口创建之前,我们在主进程中本地起了一个node server,用来跑web应用。
对于渲染进程,主要是基于React,Redux写的,Web Worker用于处理复杂计算,避免阻塞页面渲染。除此之外,我们还启了一个background进程,用来执行比如说文件监控这样的活动。
对于功能模块的实现,主要看下调试功能。对于调试,NFES DevTools既提供开发态代码的调试,也支持生产态代码的调试。
3.1 开发态调试
开发过程中的调试主要包括以下几点:
对于web和Node.js代码调试功能的实现,由于Electron自身提供的调试webview的api功能比较弱,不能满足需求,所以我们决定直接使用Chromium提供的能力。
相信大家应该都用过chrome开发者工具,其实它本质上就是一个web app,通过Chromium提供的远程调试协议,开发者工具就可以和chromium基于WebSocket进行通信,如上图左边所示。
调试功能也是基于这个协议实现的,但是如果让调试界面直接和Chromium连接会有两个问题,首先是我们没办法完全控制调试过程,不能主动向Chromium发送指令;其次是,Chromium提供的WebSocket server只允许一个client跟它连接,多个client就会出现链接已经被占用的情况。
为了解决这两个问题,我们在调试界面和Chromium之间做了一层拦截,如上图右边所示,首先起了一个WebSocketserver,让它充当Chromium跟调试界面连接;然后通过接口获取到真实的WebSocket调试地址,起了一个client让它跟Chromium链接。通过这种方式,就可以拦截掉所有调试界面和Chromium之间的WebSocket通信,之后做一个转发就可以了。
3.2 生产态调试
生产态调试功能是为了帮助开发人员更方便地调试线上代码。通常,调试线上代码会选择使用fiddler,将线上文件代理到本地来进行调试,如下图所示。NFES DevTools也支持这种方式,但是这里想跟大家聊一下另一种调试方法。
如下图所示,开发人员在发布NFES应用的时候可以选择同步生成一个生产态调试环境,这个调试环境和发布到线上的是一致的,但是多了sourcemap。
当开发人员需要调试线上的代码的时候,可以开启代理功能,开发人员设置好浏览器代理后,我们会拦截浏览器中的http/https请求,把其中与NFES应用相关的请求代理到生产态调试环境中,对请求头,响应头,返回值作出相关处理后再返回给客户端,这样开发人员就可以方便的使用sourcemap调试线上代码。
代理功能的实现是在background进程中,我们基于Node.js搭建了代理服务器,并将拦截到的请求数据存储在nedb数据库中,因为请求量可能比较大,并且需要根据请求状态的变化对数据进行更新。
数据库插入或者更新数据之后会通知WebSocket服务器,实时发送数据到渲染进程,然后在Web Worker中计算好需要展示/更新的节点信息之后,重新渲染http请求列表。
四、结语
本文简单介绍了Electron,由于它整合了Chromium和Node.js,所以基于Electron,可以使用web技术开发跨平台的桌面应用。我们也了解了Electron的工作机制,以及在开发过程中可能会遇到的白屏,多进程,数据持久化,安全性等问题/场景。
另外也分享了Electron在NFES DevTools中的实践,包括对Electron,Chromium,Node能力的应用,希望可以为想要开发Electron应用的小伙伴们有一点启发。
【推荐阅读】