一、诞生背景
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)关注我们。