语雀 App 跨端技术架构实践

6,133 阅读14分钟

1. 语雀整体介绍

1.1. 简单介绍

首先介绍一下语雀的整体情况,语雀是蚂蚁集团推出的一款笔记与文档知识库的管理 & 协同工具,目前整个集团内部的的员工大约有 10 万多人日常也在使用这个工具,同时也在对外提供服务。

如下图所示为语雀基于 Electron 推出的 Mac 和 Windows 桌面端,还有应对移动端工作场景的小程序。另外,从去年开始我们着手开发了移动端 App,并于今年 2 月份顺利发布上线。图中右侧展示的是 Android/iOS的 App 截图。 image.png

1.2. 语雀整体技术选型

接下来我将介绍一下语雀的整体技术选型。语雀在内部名称为 Skylark,它的整个项目位于一个大型的 Codebase 中。其中的服务端、桌面端、移动端等所有代码包含在这个 Codebase 中,如下所示: image.png

其中服务端采用 Node.js、Egg.js,还有一些服务是基于 Java/Kotlin 开发的,但是这部分目前相对比较少。PC 端主要采用 React 技术栈,桌面端采用 React 和 Electron,小程序采用 React 以及 H5 离线包。大家可以发现,整个技术是基于 JavaScript 或者TypeScript来做的。

整个语雀技术团队没有区分前后端或客户端,大家都是全栈式地做产品研发。一个团队成员可能既要完成一个需求的服务端部分,同时也要完成前端的代码研发。在此基础上我们进行了移动端 App 的设计和开发,这部分内容我们接下来将详细介绍。

2. 语雀 App 架构推演 & 设计

2.1. 阿里云 mPaaS

语雀 App 的架构是如何设计的呢?首先整个集团已经有一套比较稳定的移动端框架 —— mPaaS,该框架是基础的移动端开发框架,提供了丰富的移动端基础能力,如:配置开关服务、Push服务、基于长连接的 Sync 服务、H5 容器&离线包、移动分析和移动网关等基础服务。它现在在阿里云上也对外提供服务,也有很多三方公司都基于这个框架来开发自己的 App: image.png

2.2. 移动端架构思路

在这样的技术品牌打底的基础上,我们进行了移动端的架构设计。首先选用阿里云上 mPaaS 作为底层的框架基础,然后客户端的 Native 框架层尽可能轻量、与业务解耦: image.png 接下来针对渲染层,我们考虑动态或者Hybrid方案,比如要根据文档阅读页或编辑器的业务特性来选用具体的框架。最后还要考虑同构开发,因为现在已经有很多的多端业务模块在运行中了,如果能够实现同构开发,那么这些代码或者经验都可以被复用。

2.3. 移动端方案对比

下图为移动端的各种开发框架对比,相信大家已经在很多场合都看过了。其中基于 H5 Hybrid 的方案( H5 或者离线包),其性能稍微差一些。纯 Native 的性能虽然很高,但是开发成本也相应增加,上手也比较困难。现在一些公司也有自己内部的 Hybrid 开发方案。 image.png

2.4. 语雀的选择

那么语雀是怎么选择呢?首先我们希望语雀的体验是比较流畅的,能够贴近 Native App 的各种体验;其次希望能做到一次编写,双端运行,兼顾开发效率;然后就是能够兼具 Web 的研发体验,使得团队的 Web 技术栈的同学能够快速参与进来开发;最后是期望能够同构复用,这样之前沉淀的经验、模块可以直接复用。

综合各种因素,结合实际的业务需求考虑,我们最终选择了 ReactNative 作为业务层开发框架。

2.5. 语雀 App 架构图

接下来我为大家介绍一下语雀 App 的整体架构,如下图所示。最底层是 mPaaS 的基础设施,前面已经提过了,其中包含了一些通用基础组件。中间层是 Native 基础能力层,这一层提供了基础的账号管理、统一资源管理、动态化能力、各种 Bridge 以及其他服务。 image.png 接下来我们可以看到两个向上的箭头,RCTBridge 负责把基础服务通过 ReactNative Bridge 接口暴露给 ReactNative;JSBridge 则是将这些服务通过 H5 JSBridge 同步暴露给 H5。然后 ReactNative 和 H5 之间通过 RCTEvent 进行双向通信。

ReactNative 层主要实现页面生命周期、业务跳转、页面呈现等;H5 主要用来承载对更新速度等要求比较高的业务。再上层是 CI/CD 以及 DevTools、环境切换、单元测试等,充分支撑语雀的上层具体业务。

2.6. 三层架构

image.png 如图所示可以看出整体是一个三层架构。在 Native 层我们采用 Kotlin/Swift 来实现。ReactNative 层采用 TypeScript/同构 Web 方式来实现,这一层主要用来实现列表或者对流畅性、体验要求比较高的页面。而 H5 层主要结合语雀的具体业务特性来使用:比如要用ReactNative来实现一个富文本编辑器或者阅读器,那么它的代价是非常高的,这种情况就使用 H5 来实现;对页面加载速度要求比较高的 H5 页面,我们使用离线包来做页面资源加速。

2.7. 通用 JSBridge 设计

接下来介绍一下通用的 JS bridge 设计,如下所示: image.png 在 Native 层,RN 和 H5 共用一份 JS Bridge 实现。底层的一些通用服务能力,分别通过两个 Bridge 暴露上去,可以让 H5 和 ReactNative 直接调用。上图右侧展示了 H5 和 ReactNative 调用 Bridge 的代码,其实除了第一行不一样之外,后面的 Bridge 调用和结果处理都是一致的。

2.8. ReactNative ↔ H5 双向消息流动

在业务开发中,不可避免会遇到 ReactNative 页面和打开的 H5 页面之间的交互和通信。我们研发了双向消息流动机制,可以在两侧均使用 postMessage/onMessage 来收发消息。

比如在进入语雀富文本编辑页面后,底部是一个 ReactNative View,上面嵌入了 WebView/WKWebView 用于展示编辑页。这就涉及到了 H5 和 ReactNative 的双向消息流动: image.png 如上图所示,蓝色的箭头是 H5 调用 ReactNative,红色箭头是 ReactNative 调用 H5,双方通过 postMessage/onMessage 互通消息,并且支持 callback 处理回调。

3. 跨端同构实践

前面有提到,语雀 App 技术选型的时候就考虑到同构开发。那么在语雀 App 中,有什么场景是涉及到同构的呢?我们在具体的业务研发中,遇到了几种典型的同构场景,接下来简单介绍一下同构的实践经验。

3.1. 基于 ViewModel 同构列表页

如下图所示,我们看到一个Web 页面,其中包含了一些列表,每个列表项包含操作选项入口,用户点击操作菜单后,可以对列表进行排序、筛选或者删除等操作。小程序和移动端App 也是这样,这三个页面的共性就是它们都是一个列表页,同时需要请求网络加载数据,另外可能还包括分页操作等。当然这里没有特别复杂的交互,点击操作菜单可以进行删除等操作。我们知道了这些共性,就可以方便的进行同构开发。 image.png 具体列表页怎么基于 ViewModel 进行同构开发呢?如下图,我们抽出通用的 ViewModel,在其中做 loading 管理、数据请求、用户操作管理等。网络请求管理可以通过 recentListAll 请求,通过 state 和 handlers 分别暴露数据和具体操作。这样最后返回的是状态集和一个操作列表: image.png image.png 在上层使用的时候,比如说我们使用这样的 ViewModel 渲染的最终页面时候,在各个平台上只需重写 UI 渲染部分,返回对应平台的 View/div 即可。通用的重逻辑、重请求的代码都是同构开发的。

3.2. Request 请求同构

语雀业务研发中的网络请求在接口层面是同构的,这样可以保证三端代码和开发模式的一致性。具体同构方式如下:

  • 在 isomorphic 中定义通用的接口文件,其中使用 fetch/get/post/... 等方法发送请求;
  • 在各个平台上,实现具体的 fetch 接口。如:Web 侧使用浏览器的 fetch 接口,小程序侧可能调用 AlipayJSBridge,ReactNative 侧则调用 LarkRCTBridge;
  • 在构建阶段,通过 alias 将 request.js 在各个平台进行重写。

image.png

3.3. 消息三端同构

接下来介绍一下另一个同构场景:消息通知三端同构。比如我们在 Web 侧有这样一个消息列表,只要别人关注了我或者评论了我的文档,就会产生一个消息。小程序和 App 侧的 UI 也是类似的。还有一个场景就是 App 消息推送,别人对我的文档进行操作时,要在移动端设备系统通知栏展示出来: image.png 那么在这种情况下我们该怎么做同构开发呢?语雀现在有 70 多种消息,如果要为三端都写一份实现的话,可能要写 230 多次。这显然是不现实的。此时就可以基于同构的方案来做。同构部分包括各种消息的同构 Builder 生成器,分别通过模板 Notification Context 构建三端渲染结果:服务端得到消息字符串用于展示,在 H5 或者 Web 端渲染出原生的 div、span 等元素,在 ReactNative 侧渲染出 View、Text 等 View。 image.png 举一个例子,比如在下图所示的 Context 上有一个 buildActor、buildSubject 和 buildLink 来渲染用户头像或者链接等。接下来服务端、小程序和 ReactNative 提供三个 Context 实现就可以了: image.png 上面介绍的一些典型的同构场景,在 App 开发中很好的验证了架构设计阶段提出的同构思路。在实际研发中,进行模块、代码的复用,对研发效率有很大提升。

4. 子应用设计

在实际的业务研发中,有些业务模块对更新时效性有较强的要求,另外技术侧也希望一些业务模块能够做到独立维护、独立交付,同时能够按需加载,降低主应用负荷。在此基础上,我们进行了子应用的方案设计来满足这种场景。

基于 ReactNative 的动态加载特性,我们设计了子应用架构。使得 App 中对更新率要求较高的场景,进行子应用化动态加载。如下图,RN 侧主应用代码和子应用代码可以分别维护,子应用可以随 App 打包预置,也可以通过 CDN 进行动态下发加载,较好的满足了语雀的业务场景。 image.png

5. 性能 & 稳定性 & 交付

5.1. 性能调优

我们在实际开发中,遇到了一些性能问题。首先是 App 启动速度优化,App 启动时一般需要展示闪屏页、隐私协议授权页、权限授予等,我们将闪屏页和主 Activity 合二为一,同时采用 Pipeline 方案定制启动细节,最终启动速度得到了较好的优化。

第二个就是小记编辑器启动速度优化。前面说到编辑器是利用 H5 来做的,但是如果用户有了灵感,想要做一些记录,如果编辑器启动慢,那么用户是很恼火的。为此,我们对小记做了一个单独的编辑器,通过离线包将其预置到客户端,或者通过动态下发的方式更新,能够第一时间快速拉起。

另外,我们还进行了 WebView 的加载优化,对一些通用 JS 和 CSS 做了一些资源包的预加速。结合 WebView 预创建 + 循环复用的方案,对 App 端内 H5 页面的打开速度,进行了较大幅度的提升。

image.png

5.2. WebView 预创建 + 循环复用

语雀 App 中,像文档阅读页、编辑页等复杂页面均是 H5 实现。用户一直反馈在端内打开速度很慢,经过我们实测,在 Android 较差的机型上,打开一篇文档耗时有时达到 5s 左右!我们结合业务特点,采用了 WebView 预创建 + 循环复用的方案,对 H5 的打开性能有了较大幅度的提升。

如下图所示,App 启动后,启动 WebView 预创建池,预创建出离屏 H5 骨架屏 —— 该骨架屏是一个单页应用,包含了阅读页、新建页、个人页等端内高频使用的页面。在用户打开相应页面时,直接从池中取出,识别出打开场景后,进行 replace 路由切换即可。由于页面所需的资源已经预先加载好,在打开阶段,只需拉取对应的业务数据渲染即可,大大提升了页面的打开速度。 image.png 在方案上线前,我们对主要的场景做了 FCP 耗时埋点作为优化基准,上线后 Android 平均耗时整体降低 67% 左右,iOS 平均耗时整体降低 70% 左右,整体效果还可以。不过有一些场景无法用到骨架屏,比如用户进入后立即访问 H5 页面,如果这时候骨架屏还没有 ready,那么体验会降级到未优化前,这块我们后面会继续想办法进行优化。

5.3. 稳定性监控

通过 mPaaS 埋点 SDK 统计埋点信息,我们通过 mPaaS 监控后台能够看到一些基础数据,同时会把这些埋点数据流到内部监控大盘、钉钉群日常播报,也可以通过语雀实时上报通道进行ErroeBoundary/JSException实时播报,具体如下所示: image.png

5.4. 研发 & 交付效率

语雀 App 在三层架构在实践下,较好的保证了的研发效率和交付效率。我们每周会发布一个内测版本,小功能能够快速滚动内测发布,内测用户也能够积极反馈。同时,我们每两周会发布一个小版本,每个月可以发布一个大版本。

现在整个架构的 Native 侧是重服务轻UI:Android和 iOS Native 实现的模块,基本上全是服务型的代码,通过JSBridge 暴露给上层。ReactNative 侧则是重 UI 轻服务:它只是做一个单纯的渲染层,Native 服务层接口 & 能力稳定之后,很少变动,上层就可以用 ReactNative 随着业务快速滚动开发交付。

image.png

5.5. 交付质量

交付质量这块,作为 CI/CD 的一部分,我们的 QA 同学提供了非常充分的自动化测试,每天会进行自动化测试巡检,用最新的包进行滚动测试验证,同时出具报告,有问题的情况会第一时间通知开发同学修复。如下图是自动化测试报告的截图: image.png

6. 总结 & 后续计划

日常开发中,可能需要一些 Native 组件,常见的情况是,社区没有或者社区组件能力不匹配业务需求。比如语雀 App 底层的基础服务、UI 组件都是由 Native 提供的,语雀现在提供了 99 个Native JSBridge。还有一些 UI 组件需要 Native 提供双端的实现,比如NebulaWebView、ImagePreviewer、PullToRefreshView 等:

最后做一个简单的小结,我们提出三层架构的最主要思路有三个具体的思考点:一是结合业务的特点和技术储备去设计整体架构;另外就是可以尽量复用手头或者已有的组件或者社区组件;第三个是结合语雀现有的代码情况和团队整体研发匹配情况。

在整个过程中,我们提出的同构设计思路和开发模式,在开发实践中得以充分使用,可以说到了较好的验证。后续我们将持续关注性能和体验,比如将跟进社区,升级 Hermes 引擎和 Fabric 架构,同时持续进行性能优化。欢迎下载语雀 App 进行体验!