京东 618:ReactNative 框架在京东无线端的实践

阅读数:12193 2017 年 6 月 17 日

一、诞生背景

1. 无线开发的痛点

React Native 最近两三年之内整个框架在业界应该说是非常热门,很多团队、大公司都在做 RN 的一些研究开发工作。先一起回想下在 React Native 框架出现之前,互联网 APP 开发是一种什么样的模式。最初,大多数同学应该是用原生开发 Android 或者 iOS,再加上 HTML5 内嵌的方式,即 Web APP。之后又衍生出了 Hybrid APP,基于 PhoneGap/Cordova 框架实现了 WebView 的能力强化。不知道大家在做这种开发的时候,有没有遇到过一些瓶颈或者一些痛点,反正我们的团队是遇到了很多。这里总结一下之前传统的方式有哪些问题。

第一,效率低下。因为无论是 Android 还是 iOS,使用传统的原生开发都有一定的开发门槛。而且代码上不能复用,这意味着任何一个业务要在 Android 和 iOS 各做一次开发,测试和业务开发工作都不能复用。

第二,性能比较差。用传统的 H5 开发方式,受限于 WebView 容器的一些瓶颈,导致无论在页面加载还是用户体验上,相比原生应用有比较大的差距。

第三,灵活性不够。因为传统原生开发意味着任何改动都需要发版,在 Android 上因为像国内应用商店非常多,而且涉及到各种不同的渠道包,所以发版成本很大;在 iOS 则受限于苹果的审核机制。对我们来讲,任何这种线上问题处理起来都非常痛苦。

最后,接入困难。因为 Android 和 iOS 平台有差异,所以任何一种垂直业务接入 APP 的成本非常高,很多业务代码和业务流程并不能复用,造成业务团队的开发、接入成本非常高。

2.React Native 登场

说了这么多痛点,我们也在反思到底需要一种什么样的框架来解决这些问题。非常幸运,我们在 2015 年的时候注意到 Facebook 发布了非常具有颠覆性的 RN 框架。简单来说,这是一种跨平台的移动应用开发框架。在当时它非常有颠覆性,因为它最大的特点就是完全用 JavaScript 进行应用的开发,但是最终会渲染成原生的组件。对开发者来说,这意味着你拥有了 Web 开发的效率,同时兼顾了原生的性能。这对我们当时业务的吸引力非常大,这个框架一经推出,国内外很多公司都在用,像 Facebook 自己也在用。在国内,手机百度、手机 QQ、京东 APP 也很早就进行了开发。RN 对我们团队来讲都有哪些优点,或者说为什么要用它,这里大概总结了以下四个原因。

第一,学习成本低。因为它的开发基于 JavaScript,JS 语言本身在开发者当中有非常良好的群众基础,任何一个有经验的前端团队可以快速地上手 RN 开发。

第二,多端代码复用。因为所有的业务都用 JavaScript 开发完之后只有一份代码,然后通过编译打包机制直接部署到不同的平台,如 Android、iOS 甚至 Windows 平台。

第三,接近原生的性能。开发者使用 JavaScript 进行 RN 框架开发,开发完之后在再通过中间的虚拟 DOM。这个实际上是它核心所在,传统的 H5 的应用是跑在 Web View 的容器当中,容器中需要维护一个真实的 DOM,而真实的 DOM 上每一次操作都会有回流 (reflow) 和重绘 (repaint),效率并不高。Facebook 最有颠覆性的一点就是提出了一个虚拟 DOM 的概念,把整个 DOM 放在内存当中,然后通过高效的 diff 算法来计算比较哪些 UI 组件需要更新,最终只对这些需要更新的组件进行真实操作。经过测试,采用 RN 框架,无论是加载性能还是页面滑动性的用户体验上,都比原来 H5 的方式要好很多。

最后,社区活跃。除了 Facebook 之外,GitHub 上有很多第三方的团队、个人、公司开发贡献了很多非常优秀的第三方组件,它的社区是非常健康、非常活跃的。

3.React Native 的局限

不过现实是残酷的,即便确定了用 RN 框架做业务开发,在实际的开发当中也发现了 RN 的一些不足。对我们的业务来讲,最不能接受的主要是以下四个方面。

第一点,RN 框架原生并不支持 Web 端。这意味着如果一个业务需要同时上 Android、iOS 和 H5 页面的话,那除了用 RN 之外,还需要用传统的 H5 或用 ReactJS 框架再做一次开发,这样效率是非常低的。

第二点,RN 框架官方并不支持热更新。虽然现在有很多第三方方案,比如微软的 CodePush,但是官方并不原生支持热更新,而热更新对我们的业务来说也是非常重要。

第三点,Facebook 给出的官方 RN API 不能完全满足业务快速的发展。它只给了一些很基础的 API,但业务中经常会用到的一些多媒体,比如录音、录像、视频播放文件以及文件上传、压缩、加密等等,这些都没有提供。

最后,前面提到 RN 框架性能非常不错,比 H5 好很多。实际上经过真正的业务开发后,发现 90% 的场景下 RN 的性能非常棒,可以满足我们的业务需求;但是在另外的10% 的场景下,特别是一些交互非常复杂、页面非常复杂、需要频繁的更新、需要一些手势交互的场景,RN 仍有些内存跟性能的瓶颈。

4. 解决方案:JDReact 三端融合平台

既然 RN 有优点也有缺点,那怎么办?

我们的解决方案是基于 RN 框架进行了深度定制和二次开发,逐步打造了符合京东业务的 JDReact 三端融合平台,主要的工作是以下四大方面:

第一,把 RN 的核心 Base 库拿来做裁剪和二次开发,把不需要的功能删减掉,把性能、兼容性、稳定性的问题修复,包括也支持了拆分打包。

第二,在后端搭建了一个功能支撑平台,帮 RN 框架增加了灰度更新升级、数据监控以及降级容灾功能,这些对业务来说是非常重要的。

第三,基于整个 RN 框架,结合京东的一些业务特点,封装了一套自己的业务组件,包括 UI 公共组件库。目的是为了让垂直业务开发者可以很快地使用框架进行业务开发,完全不用关心设计的样式跟交互,可以快速接入业务。

第四,打通 Web 端,实现了一套 RN 框架向 ReactJS 转换的工具。可以做到一次代码编写,直接部署到 Android、iOS 跟 Web 三端。

二、JDReact 三端融合平台全解析

1. 整体架构

下图所示就是整个 JDReact 三端融合平台的架构图。

最下面是一个后端接入平台,包含刚刚提到的灰度更新、降级容灾、数据采集和持续集成,这些是由服务端提供的一套服务。中间这一层是提供给内部开发者的一套完整的 SDK 开发工具,里面除了一些 API 之外,也封装了大量的京东定制功能组件,包括 UI 公共组件。其中还有一块是 Web 转换工具,提供了一套 RN 转换的脚本。业务开发者完全不需要关注这些细节,只要关心他自己的业务逻辑,就可以直接开发出覆盖三端的应用。最上面的业务层就是京东 APP 所有使用三端融合平台开发的业务,这些都可以直接部署到 Android、iOS 和 Web。

2. 改进和优化实践

前面主要介绍了整体的平台架构,现在开始来分享一些干货,就是我们在开发过程当中团队遇到的 RN 的一些问题,包括如何改进跟优化的一些实践。我列了一些功能点跟大家一起分享。

功能裁剪

有同学抱怨过 RN 库太大了,所以拿到 RN 的第一件事就是裁剪。对 Android 平台来讲,除了把 RN 的基础库裁剪以外,很重要一点就是要把方法数减少。因为 Android 平台 dex 有方法数限制,一旦超过 65K 就需要拆分成多个 dex,整个应用的安装跟加载都会有性能问题。所以,要对 Android 方法数进行严格控制,我们的做法就是根据业务情况,把一些用不到的组件方案中的功能组件删除。其中最重要改动就是把 Android 中 support-v7 和 stetho 库依赖给去掉,去掉之后不仅大小减小了很多,而且方法数减少了将近 7000。除了移除这个功能库,很重要一点,因为不是一个全新的 RN 应用,需要跟现有的体量很大的 APP 做集成整合,所以尽量让一些依赖库复用主站中依赖库,比如 fresco、okhttp 等。一来缩减包的大小,二来避免包的冲突。但是主站中的版本很可能跟 RN 中引用的版本有差异,需要中间做一层适配层,把这些差异尽量抹平,保证这些功能和方法都能工作。

加载性能优化

虽然说 RN 框架号称比 H5 的加载性能快很多,但实际开发中发现在 Android 的一些低端机型上,加载速度还是达不到原生体验,极端情况下甚至会出现白屏。主要原因是业务 jsbundle 比较大,RN 框架在加载 jsbundle 和通过 JSCore 解析 jsbundle 时耗时太长。当用户看到真正业务页面之前会出现长时间的空白页面。

当时提出了两个解决方案,第一种方式是实现一套预加载机制。预加载机制就是在用户真正进入业务之前,把 jsbundle 提前加载解析,提前把 RootView 生成。简单来说就是用空间换时间。但这样做并不是所有的业务都适合,因为会带来一些内存增长,所以一般在很核心很重要的业务采取预加载机制。第二种方式是修改了 RN 框架底层库,在 RN 框架开始加载 jsbundle 文件时,显示一个 loading 的进度提示用户正在做加载的动作。当 JS 文件加载并且解析渲染完成之后,把进度条去掉,最终被页面展现给用户。这样虽然等待时间并没有减少,但是用户体验会好很多,整体的时间从收到的反馈来看还是比 H5 要好很多,这是我们做的一个优化点。

内存优化

我们还做了一件很重要的事情,就是内存优化。在 RN 框架开发中碰到的最大的坑就是内存这块,因为业务中会经常碰到 ListView 的使用,根据这些业务的需要,可能要加载很多页,两页、三页、甚至可能会无限加载。这种方式在早期的 RN 版本当中肯定会引起 OOM(OutOfMemory)崩溃,原因是在 RN 的早期版本当中并没有对 ListView 做内存复用。这意味着 ListView 滚多少,图片都会在内存中,当页面加载地越多,出现 OOM 崩溃的几率也越大,这是一个非常不能接受的问题。

在 RN 的早期版本,我们团队在 JS 层实现了一套内存回收。它的原理跟原生当中的原理也差不多,就是当页面划出两个屏幕之后,会强制把图片和内容进行回收,用一个空白的 View 替换。当内容划到用户可见的屏幕范围之后,再把图片给加载出来,这也是原生常用的一种内存回收的方式。修改后的效果很好,无论页面加载再多,都不会出现卡顿和 OOM 崩溃。在 RN 的新版本(0.43 之后),引入了一个新的 FlatList 组件。这个组件完全解决了 ListView 的内存回收问题。它的实现机制和我们的方案类似也是在 JS 层中做内存回收的动作。所以给大家建议,如果开发中碰到类似的问题,完全可以升级到最新的 RN Base 0.43 以上使用 FlatList 组件。如果版本比较低的话,那就需要自己实现这套机制。

第二个比较大的内存问题就是图片,iOS 平台可能相对好一些,在 Android 问题会相对多一些。RN 的底层图片框架库用的是 Fresco,而我们主 App 中用的也是 Fresco 底层库,这里就会有些问题。第一个就是重复初始化,这也是当时业务开发当中碰到的问题。当主 App 中的 Fresco 进行初始化之后,如果 RN 中也进行一次初始化,实际上之前那部分内存并没有被释放,会出现内存泄漏。我们做了专门的检测,避免 RN 重复初始化的问题。第二个也是跟 RN 框架里面的实现有关系,因为它采用的图片编解码用的是 ARGB_8888,这种方式支持 Alpha 通道。但实际上大部分情况下可以采用 RGB_565 编码,虽然丢失了 Alpha 通道,但是图片在内存当中的大小可以减少 50%。不过有些业务可能也真的需要一些透明的背景,需要 Alpha 通道,所以也提供了一些 API 来针对特殊图片,让它采用 ARGB_8888 进行编解码。这样既解决内存问题,也满足了业务的需求。

最后一个经验就是在所有的 RN 页面退出之前,建议强制调用 Fresco 框架的 clearMemoryCache 方法,通知 Fresco 清除内存缓存。可以保证 GC 及时地把这些图片内存给回收掉,避免整个 APP 的内存占用过高,经过实践验证这也很有效地解决了内存问题。

拆分打包

关于拆分包,因为目前我们采取的方式是每一个业务打成一个 jsbundle 文件,这意味着业务越多,jsbundle 文件会越大。而这些 jsbundle 文件当中,业务的代码其实占比很小。百分之七八十都是 Facebook 提供的一些公共组件库。我们的做法是在编译打包之前,把这些公共组件库先抽取出来,放在一个 common jsbundle 里面,然后业务只保留业务相关的一些 jsbundle 文件。最终在真正的加载之前,做一个简单的合并动作,这样业务越多,这种优化的效果就越好,可以有效减缓 jsbundle 文件大小的增长速度。

性能优化

除了内存之外,最关心的就是性能。前面也提了 RN 的性能其实比 H5 要好很多,可以满足我们 90% 的场景,但实际上还有 10% 的场景,RN 做的并不是很好。主要也是因为整个 RN 的机制,它虽然是最终渲染成原生的组件,但是 UI 的控制还是在 JS 中做的。受限于 JS 单线程一些限制,当有一些很复杂的交互、很复杂的手势或者快速的滑动,很有可能引起 JS 中的阻塞,造成动画的一些渲染的数据不能及时同步到原生当中,造成了整个页面的卡顿。

建议的方案有三种,第一个做 RN 的 Base 升级,把 RN 升级到最新的 0.45,它会采用了一个新的叫 Yoga 的引擎。这种引擎是完全用 native 实现的,可以把大部分的动画渲染和交互放在原生的线程中做。经过测试,采用了 Yoga 引擎,整体的渲染性能可以提升 30% 以上。

第二种方式,有一些非常复杂的一些交互,比如左右滑动结合上下滑动一些手势,如果用单纯用 RN 做,很容易碰到一些手势冲突的问题。所以把这种组件原生化,完全用原生实现,所有的交互跟手势控制全在原生做。这样做就可以达到非常完美的性能,但同时也需要原生开发团队介入。

最后一个经验就是尽量使用 Animated 这种动画类,减少 JS 控制的 UI 数据同步,避免 JS 线程阻塞。

版本检测

另外在 jsbundle 文件当中增加了一个 version 文件,解决版本冲突检测。因为要支持线上更新,就意味着需要把每一个业务 jsbundle 文件做一套完善的版本控制。需要知道当前这个 jsbundle 文件的版本号是多少,可以跑在哪个客户端的版本当中,可以支持的这它的 RN 底层库是多少。这些信息都会记录下来,然后在每一次的升级之前做版本检测。这可以有效地避免线上不同客户端和不同 RN 版本之间的版本冲突问题,可以支持线上的灰度升级。

兼容检测

RN 其实有最低版本支持,像它的早期版本在 Android 是支持 API 16 以上,iOS 是 iOS7 以上。其实我们的主 APP 要支持的版本会比他更低一些,所以需要在主 APP 中做一些保护和判断,一旦检测到用户的版本不支持 RN,就需要做一些降级处理,比如说把入口关闭或者跳转 M 页。这样最大的程度避免不支持 RN 版本的用户出现崩溃的情况。RN 其实可以支持 x86 芯片,但是考虑到如果要支持 x86 的话,需要增加一套基于 x86 的 so 文件,会对包大小有影响,所以对所有的 x86 做了降级。

原生能力扩展

前面刚才也提到了我们的业务非常多样,很多的能力 RN 并不支持。所以基于 RN 框架我们扩展很多业务上用到的原生组件,比如做了整个多媒体的视频播放、视频录制、音频播放、音频录制等组件,还有一些文件上传、语音识别等。在 RN 提供了这套 JS 的接口,给垂直业务团队快速开发和使用。

3. 通用组件库封装

我们也结合自己的业务做了一套通用组件库的封装,例如京东当中的用户登录、购物车、收银台等等业务,在 RN 中做了一套组件的封装。把所有的接口都提供了 JS 的 API,样式和交互像常用的下拉刷新、对话框、按钮等等,也提供了一套通用的样式组件给开发者。在做业务开发的时候,完全不需要关心这些样式怎么画、颜色怎么搭配,只需要关注业务逻辑。剩下的事情由框架做,这可以提升整个业务开发的效率。

4. 三端融合

刚才前面提到了很重要的一项工作就是克服了 RN 不支持 Web 端的问题。我们做了一套 Web 转换的工具,打通了三端。其实在业内三端融合也有广泛的研究,方案主要有三种。

第一种方式,就是在 RN 跟 ReactJS 之上再封装一套轻量的跨平台的抽象层,像微软发布的 ReactXP 就类似于这样的架构。使用这种架构,意味着所有 API、类、组件都不能用 RN API,必须要用新的定义的接口,而且目前 API 支持也不是太多,还在完善中,所以没有采用这种方式。

第二种就是 ReactJS 做开发,之后通过工具转换成 RN,这种方案适合于比较偏重 H5 业务的一些团队,因为他优先需要上的是 H5 页面,用户体验比较偏重 H5。通过工具向 RN 转换其实是个有损转换,因为 RN 支持的样式实际比 CSS 样式少。从 ReactJS 向 RN 转换的话,可能会丢掉一些属性和布局。

第三种方案就是先用 RN 做开发,开发完之后再通过 WebPack 工具向 ReactJS 进行转换。这种方式的好处是可以优先保证 RN 中的体验,而且 RN 的样式支持是 CSS 的一个子集,这意味着从 RN 向 ReactJS 转换不会丢失功能和属性,所以业内更多的方案也是采用这种方式。GitHub 上有一些类似的开源框架。但它们支持的组件并不是太全,不能完全覆盖我们的业务,所以我们自己实现了一套。包括之前说的所有的原生组件,它只有原生部分,我们也增加了 JS 部分的实现,使我们的框架可以完整、功能完全没有丢失地转化为 Web 页面。

5. 灰度更新

下面简单介绍我们后端的接入平台在服务端增加的灰度更新控制。发版之后,如果需要做一些 RN 组件更新,可以通过后台的更新服务器做一次支持这种灰度的更新。它大概的流程就是首先由 APP 端发起更新的请求,发送到路由控制,路由控制负责控制用户是不是在灰度比例范围之内。如果符合灰度策略,把这个请求转到服务端处理,服务端根据客户端上报的 jsbundle 文件的版本跟服务端部署的版本做一次比较,看有没有适合这个业务的新版本。如果有,把这个升级的版本号以及下发地址回传给客户端,客户端会直接根据下发的下载地址从云存储上下载升级包,完成整个升级过程。因为用户量比较大,所以每一次更新一定要有一个灰度策略,根据灰度比例逐渐放到全网,这是非常重要的。

6. 降级容灾

我们把降级容灾定义为两种:一种叫被动降级,一种叫主动降级。所谓的被动降级是指客户端确实不支持 RN 框架,每次加载 RN 框架都会出现问题,那必须要进行被动的降级,跳转到对应的 H5 页面,使得对业务的影响降到最低。这种降级逻辑是在客户端当中做处理的,就是前面介绍的兼容性检测。第二种是主动降级,很可能在业务开发的时候会发现一些上游的接口出现了问题,导致客户端中的某项业务不能正确地运行,这个时候就需要由服务端控制对这项业务进行精准的降级。我们会支持多个维度灵活的配置,可以根据客户端的版本号、客户端的型号,配置灰度比例、白名单,精准地对某些用户的某些业务或者某些地区的某些用户进行降级,减少业务上的损失。在一些非常大的促销的时候,像双 11、618 这种峰值非常高的时候,可能会采用这种方式。

7. 持续集成

这主要是我们内部的一个开发模型,把所有 RN 的基础库,包括自己提供的一些公共组件库、公共 UI 组件库都部署在内部的 NPM Server 上。每一个接入的业务开发者、每个业务都会有一个独立的 GIT。因为在我们内部,其实业务开发团队可能会很多,我们的团队是负责维护框架,而业务开发团队各个部门各个地区都会有,他们会有申请自己独立的 GIT,然后从 NPM Server 上下载最新的 SDK 包进行业务开发。业务开发完成调试之后,会通过 CI 打包平台发起打包命令,然后触发 Jenkins 当中的 job,从对应的业务的规则拉取代码,进行编译打包。编译打包成功之后,把它部署到对应的 Android 或者 iOS 客户端版本当中进行整个发布。这种方式对业务开发者来说,最大的好处就是打包编译完全是脚本自动化,不需要获取客户端的源码就可以做到这个业务的开发和上线。

8. 数据监控

JDReact 三端融合平台我们也做了一套非常完善的数据监控中心。因为需要知道所有 RN 页面启动的时间、加载页面的时间、服务端返回的响应时间、界面渲染的时间。需要把这些 APM 数据上报,通过上报的数据分析,不断地优化性能。第二,也会把业务开发当中碰到的一些异常日志进行上报,可以帮助我们快速的定位问题,发现问题并部署相应的升级包。第三,因为灰度升级更新这个机制,需要有数据埋点来统计升级的成功率。有多少的用户真的是发布升级之后可以成功升级到这个版本。最后,也会针对 DAU、UV、PV 等基础数据做统计,这个主要也是帮业务方搜集一些运营数据,好做业务上的决策。

三、总结

这个框架推出有一年多的时间,到目前为止,京东 APP 当中已经有 20 多个业务正在使用这套框架,其中也有一些比较重要和常用的业务。我们整个平台也经历了去年的双 11、618,今年即将到来的 618,我们也会做更多的后台保障,在稳定性,包括降级上做一些处理,确保业务能够正常地推进。未来也希望能够不断的完善这个平台,不光是京东内部在用,一些很好的组件框架也可以开放出来,跟大家一起学习进步。

作者简介

沈晨,京东商城专家架构师、JDReact 三端融合平台负责人。


感谢徐川木环对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论

最新评论

InfoQ_772b4ebc1ef1 2019 年 06 月 09 日 23:16 1 回复
En na Good.
Eviler 2018 年 12 月 14 日 15:40 0 回复
然后,并没开源。。
没有更多了