1

哈骑士是哈啰的一款终端安全应用,本文主要介绍我们在做新版哈骑士桌面端时的一些技术架构思考和实践,分享我们沉淀的一些桌面端应用的解决方案和经验。

为什么选择Electron

前端开发者入门快

图片

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发经验,有了它,前端开发者就可以使用前端开发技术来开发桌面应用了。

支持跨端&开发效率高

图片

如上图所示:

  • Native(C++/C#/Objective-C)不管从原生体验、包的体积、性能方面来说都是最佳的选择,但是开发门槛高、迭代速度慢。
  • QT是基于C++的跨平台开发框架,跨平台应用十分广泛(Mac、Windows、ios、Android、Linux、嵌入式),众所周知的WPS就是用QT开发的。性能很好,甚至于可以媲美原生的体验,但是整体门槛还是比较高的。
  • NW也是一个跨平台的框架,但是其社区以及解决方案相对于Electron来说并不是那么强大,而且所有的非javascript编写的模块都需要重新用nw-gyp重新编译,相对于Electron来说,不是那么灵活。
  • Tauri也是一个非常火爆的跨平台的桌面端框架,相对于Electron来说还不是那么成熟,生态方面也略显青涩,兼容性问题有待考证。

作为一个跨平台的桌面应用开发框架,Electron 的迷人之处在于,它是建立在 Chromium 和 Node.js 之上的,二位分工明确,一个负责界面,一个负责背后的逻辑。虽然系统间还是会有很大的差异,需要相应地做一些额外处理,使得打包出的应用在不同系统下都能正常运转,但相比于 80% 都能完全复用的代码,这些时间和成本都是可以忽略的,开发效率直接翻倍,如果你开发一个不需要太关注底层的桌面端应用,基本不需要做底层的抹平逻辑。

图片

另外,Electron 是基于 Node.js 的,这就意味着,Node 这个大生态下的模块,Electron 都可以用。同时,跨平台也让 Electron 可同时开发 Web 应用和桌面应用,无论是 UI,还是代码,很多资源都可以共享,大幅减少了开发者的工作量。

生态繁荣&案例成熟

图片

图片

Electron生态的确很强大,各种库和工具包都为你构建一个桌面端应用提供了很多方案。

图片

当然,不止如此,现在用Electron做桌面端的案例也非常成熟了。上图已经说明了Electron应用是有多广泛了,这其中不乏大名鼎鼎、如雷贯耳的应用,例如 Postman、Skype、VScode 等。而且我敢打赌,各位看官的电脑上一定安装过用 Electron 开发的应用,如果你用的是 Mac 电脑,请在命令行运行下面的命令来检测本地采用 Electron 技术开发的桌面软件:

for app in /Applications/*; do;[ -d $app/Contents/Frameworks/Electron\ Framework.framework ] && echo $app; done

Electron生态开发技术选型

脚手架选型

关于脚手架的选择,其实也很多。

官方提供的有Electron Forge,Electron Fiddle,electron-quick-start,其实如果你的应用不复杂,可以用官方的脚手架生成一个快速上手的模版,然后就可以愉快地开发了。

当然也有一些开源的脚手架,比如electron-vue或vue-cli-plugin-electron-builder之类的,也可以让你快速的生成一个固定的模版,然后往里面填充你的内容。

个人认为,官方的脚手架工具可以用来尝鲜,学习使用,electron-vue这类工具,如果是在一个企业级的项目中使用,前期会给你带来便利,但是后期扩展不会太友好,另外就是他们是基于webpack构建的工具,在日常的开发和使用中会觉得编译得不够快(相对于Vite)。

另外就是如果你想自己完成一个项目脚手架(项目框架),完全可以凭借自己的经验或者参考开源项目的架构自己来完成一个脚手架,一来是为了更加了解Electron的构建原理,二来是可以搭建出适合自己风格项目的脚手架,后期利于扩展和丰富。

所以我们脚手架的选型就是自己来造一个Electron的项目架构,从package.json开始,用Vite+Electron+React构建一个Electron项目。

网络模块选型

Electron发送HTTP请求的方案有很多。

第一种就是渲染进程和主进程分别用相应的请求HTTP请求工具来进行网络请求,比如渲染进程可以使用fetch,主进程用net模块。这种方案的优点就是可以把渲染进程和主进程的请求分开,分工明确,而且调试也方便,渲染进程可以直接看network;缺点就是,如果要对请求进行统一封装的话,比较麻烦。

第二种就是所有的请求统一封装,如果你都使用net模块或者其他的请求工具包对请求进行统一的封装,然后主进程直接使用,渲染进程调用统一的桥接方法。这种方案就是完全可以统一请求封装,但是如果想调试的请求的话,不方便,需要在主进程来日志信息。

第三种就是,直接axios直接一把梭,它既支持node环境,也支持浏览器环境。这种方案非常方便,你就按照之前封装Web应用请求的思路去封装自己的请求模块就行,不过需要注意跨域问题。

对于上面的几种方案,各有各的优缺点,可以根据自己的场景需求来决定使用哪种方案。我们选择了axios来设计网络请求模块。

本地数据库选型

Electron的本地数据存储方式也有很多种,可以直接读写文件,也可以用相关的库,方便数据管理。一些库的对比,详情:https://www.npmtrends.com/electron-store-vs-lokijs-vs-lowdb-v...

图片

综合来看lowdb更胜一筹,所以选择lowdb做本地数据库,非常好的一点是它支持同步,不必担心数据没有写入就进行了下一步需要本地数据的业务操作。

日志工具选型

日志工具对Electron的开发也是尤为重要的,可以给你定位到一些表层无法定位的问题,所以一款好的日志工具对开发是非常有帮助的。

比较常见的日志工具就是electron-log和log4js-node,这两款日志工具我都有用过。可以看下npm的排行,这里把express-winston和logging也加上看一下,详情:https://npmtrends.com/electron-log-vs-express-winston-vs-log4...

图片

这里简单说一下electron-log和log4js-node的比较,两者上手都比较简单,log4js-node暴露的API 非常多,electron-log就稍显逊色了,另外最直观的感受就是,electron-log的日志文件路径不好找,暂时没发现自定义日志路径的方法,log4js-node有相应的方法,而且你可以自定义各种文件类型。根据使用体验,觉得log4js-node更好,推荐log4js-node。

构建工具选型

三种构建工具electron-builder, electron-forge, electron-packager 对比一下。

image.png

从这个排行来看electron-builder的确很强,electron-forge最近又更新大的版本,不过没有尝鲜,我在electron-builder上倒是踩了不少坑,可以分享给大家。所以我在开发的时候选择的构建打包工具是electron-builder,它把整套解决方案都集成了,包括打包、更新、签名、分发,基本的钩子和配置都有相应的暴露。

核心架构实现

架构概览

图片

我们整个框架是基于Eletcorn + Vite构建的,在底层依赖的安全能力和存储模块的基础设施之上设计了一层基础框架,实现构建打包,架构分层的设计,然后给整个桌面应用提供一些应用管理能力和GUI管理相关的能力,最上层就是为了一些业务场景提供的一些应用能力,包括核心的几个应用和主要的策略引擎应用(终端策略和合规策略)。

开发构建Electron是多进程架构的体系,所以我们在开发构建的时候就是构建多个进程来实现我们的应用。核心思路是通过Vite构建三个进程:渲染进程,任务进程,主进程,然后最后将三个进程融合起来,就形成了一个应用。核心代码如下:

图片

几个注意点:

  • 我们这里利用了writeBundle,就是等chunk都写入文件后,再启动Electron进程。
  • 这里没有利用Electron的命令启动,而是通过Node.js的child_process模块的spawn方法启动Electron子进程,主要是因为我们需要依赖开发环境的渲染进程。
  • 另外就是config/vite/main.js中需要对rollupOptions的external进行electron的配置,把导入包转成外部依赖,不然在启动Electron会找不到Electron的路径。
  • 在createMainServer中我们注入了全局可使用的变量,以便Electorn加载页面的时候可以使用这些变量。

架构分层

图片

因为需要跨端开发,Mac和Windows有些底层模块的实现还是有不一样的地方,所以我们在开发设计的时候将代码进行了分层设计,这样至上而下的调用在上层看来是一样的,所以我们需要磨平端上底层的差异,现阶段我们底层模块的实现是通过目录来严格区分的,这样在开发一个底层的功能的时候就可以做到各段相互不影响。

打包升级

桌面客户端相当于传统的Web应用在打包和更新这一块还是有非常大的不同的,传统的web应用几乎不用所谓的升级,浏览器刷新页面即可,但是桌面客户端就需要完整的给用户一个可以立即执行的安装应用程序,而且还要可持续迭代和更新,所以在打包升级这一块,我们也是踩了不少坑。

图片

1.关于打包
打包其实Electron的生态也是非常成熟的,如上面提到的构建技术选型,我们选择的是electron-builder,它提供了一套打包构建升级的流程,暴露了很多API,傻瓜式的配置就基本可以让你实现一个应用的打包了,唯一麻烦的就是签名和认证应用。

在Windows端我们使用pfx格式的证书进行认证,在进行打包的时候会和证书客户端软件交互,完成各个文件的签名,这样用户使用客户端的时候就是签名过的软件了。

在Mac端我们需要使用苹果认证的开发者证书进行签名和认证,配置相应的identity后,构建打包的时候会直接跟你本地的证书进行交互,然后对文件进行签名,当前我们还需要让应用可以不必严格使用 MAP_JIT 标识也能写入和运行内存内容。所以需要加入entitlements和entitlementsInherit。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
  </dict>
</plist>

到这一步其实Mac端的软件签名就完成了,但是如果应用想App Store上架的话还需要对应用进行公证。公证主要是使用electron-notarize来进行公证,启用afterSign即可,afterSign: './script/notarize.js',

下面的Apple ID就是你的开发者账号,appleIdPassword需要生成一个专用的应用密码,不要使用你本来的Apple ID密码。

const { notarize } = require("electron-notarize");

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== "darwin") {
    return;
  }

  const appName = context.packager.appInfo.productFilename;

  console.log(\`公证中...\`)

  return await notarize({
    appBundleId: "mac.hellobike.knight",
    appPath: \`${appOutDir}/${appName}.app\`,1
    appleId: "XXXXX@outlook.com",
    appleIdPassword: "XXXXX",
  });
};

notarize会根据你的配置去校验你的应用是否可以公证成功,公证的时候会和苹果的服务器进行通讯,所以需要保持网络不要断开,成功或者失败之后都会发送相应的邮件到你的开发者邮箱里面。到这里打包的核心工作就做完了,如果你需要其他个性化配置,参考electron-builder官方的文档即可。

2.关于升级
升级我们在Mac和Windows上的实现各有不同,因为相比于传统的软件,我们哈骑士会一直保活在用户的进程中,所以在更新升级的时候也会打破原本Electron升级的机制。

在Windows上其实还好,可以利用electron-updater本身的生命周期来完成下载,更新,重启应用,因为Windows的保活是用另外的服务来实现的,所以并不会对整个更新周期产生破坏性的影响。

但是Mac端的保活实现是打破了electron-updater本身的生命周期的,探究其源码会发现Electron自己的升级服务其实也是一个保活的应用服务,所以在升级之前需要将其Kill后才能完成哈骑士自己本身的更新逻辑,另外就是文件占用和锁定的问题,为此我们自研了一套更新脚本程序结合electron-updater的下载更新的能力实现了Mac端软件的升级。

核心能力沉淀

基础能力

我们在做哈骑士客户端的时候,也沉淀了一些与业务无耦合的组件和工具类,这些组件和工具在桌面端应用的场景都比较通用。

image.png

  • 本地数据库管理
    本地数据存储是业务场景中随处可见的重要功能。为此,我们封装了常用的增删改查数据库的能力,并提供给各个进程使用,以实现数据持久化存储。
  • 底层桥接
    底层桥接是解决Electron和Node无法覆盖所有应用场景的必要手段。我们在桥接层封装了三种桥接模式,分别为渲染进程调用的jsBridge能力、主进程调用dll和dylib插件的能力,以及桥接rust程序的能力。这三种模式基本上可以解决所有技术瓶颈。
  • 客户端请求
    客户端请求模块也是至关重要的。我们将其封装成了通用的http请求库,支持主进程、渲染进程和任务进程的调用,以抹平上层调用的差异性。
  • 任务管理
    由于业务场景和客户端的特殊性,我们经常需要进行本地任务管理。因此,我们将任务管理模块封装成了通用的工具类,以支持对任务的注册、启动、停止和销毁等各项生命周期的管理。

应用能力

在上面这些基础能力的组合应用下,我们形成了一个强大的策略引擎应用。

图片

该策略引擎应用实现了端上任务调度和分发功能。首先接收后台配置的策略信息,然后生成对应的任务,并分发到各个子任务中心以执行对应的策略。最后,将策略执行情况报告给服务端。

总结

Electron在哈骑士的应用非常成功,虽然在使用过程中遇到了一些问题,但不可否认它是目前最适合我们业务目标和开发资源的框架。使用Electron使需求交付效率得到了很大的提升。我们也将持续关注性能和稳定性的优化、桌面端全链路日志的完善以及增量更新升级能力等方面的改进。

(本文作者:徐涛焘)

图片


哈啰技术
89 声望51 粉丝

哈啰官方技术号,不定期分享哈啰的相关技术产出。