React Native技术在小米有品App中的应用

笔者khzliu同学,关注于大前端、跨端技术

小米有品简介

小米有品是小米旗下的一个开放的生活购物平台,小米有品App从最初的米家App内一个商城标签页经过两年的发展成为一个独立的商城应用。作为一个电商系统应用,小米有品App支撑了会场导购、营销活动、内容直播、商城交易和售后物流等业务的前端页面呈现与交互。因为电商的业务场景特点就是更新迭代速度快、动态化要求高、前端UI样式和交互逻辑经常变动,所以小米有品App在技术选型上要满足这些基本需求,同时小米有品又是一个精品电商平台,极致的性能和流畅的交互体验也是小米有品App追求的目标。

图片

React Native

React Native是Facebook在2015年3月发布的跨平台移动应用开发框架,此框架采用MIT开源协议,支持使用JavaScript语言和React前端框架来构建性能媲美原生的移动端应用,并支持跨iOS,Android,Web三端平台,采用React Naitve官方的一句话就是“Learn once, write anywhere.”。React Native另外一个特点就是社区活跃,且有大量的第三方库可供参考和使用,可大大提高研发效率。

为了满足有品业务快速发展需要,小米有品App在最初技术选型时期经过大量调研,选择了具有跨端能力、支持热更新且性能媲美原生应用的React Native框架来支持前端业务开发。在小米有品App内部针对不同的业务场景也同时保留适合某种特殊场景的其他技术方案,比如部分涉及到外部团队开发的活动类页面则支持使用Hybrid H5方式进行展示,扫一扫、登录、账号中心等功能类且不经常变动的页面则采用Native进行开发。

图片

支撑的业务

在小米有品App中大概70%以上的业务是使用React Native技术进行开发支持的,这些业务当中不仅包括核心的商城交易类页面,还包括内容直播类、分类搜索商品聚合类、秒杀等活动类以及消息服务类业务页面,未来React Native页面占比会提高到90%以上。

图片

商城交易类页面是整个商城的核心,比如产品站、购物车、结算、支付和订单、物流和售后等,这类页面整体结构和交互并不复杂,但是要求页面稳定性特别高,所以在异常处理和逻辑边界以及日志上报上需要特别注意;内容直播类页面主要负责有品用户社区运营,注重长列表的应用与优化,图、文、视三方面的显示和直播内特效动画交互等,比如社区主页,种草文章以及有品直播这类页面;活动类页面特点就是页面内楼层UI样式种类多、迭代速度快,需要在React Native的基础上增加Low-Code/No-Code技术来支持不断更新的楼层样式,另外还需要支持楼层间多级联动,App内这类页面包括S级别秒杀活动、拼团活动等;还有部分消息服务类页面,比如消息、通知和客服,这类则着重网络连通,消息实时和应用权限处理等等。

应用的技术

小米有品App从最初使用React Native到现在,大前端团队也积累了许多使用经验,比如如何能够实时的且在用户无感知的情况下进行业务更新和重载、如何管理和使用资源文件、如何扩展符合有品业务本身的组件库等。同时也面临不少挑战,比如怎么使得React Native的页面性能发挥到极致、如何能在React Native的基础上让研发更高效、如何适应动态扩增的业务场景,如何解决工程臃肿,促使测试发布标准化、流程化以及从开发到测试再到加载之后业务的隔离问题。

图片

更新与重载

早期的小米有品App体量和业务量较小,所有的业务采用的是单个React Native包进行包的构建和更新。一个完整的版本包内既有可执行的JS代码,还有一些常用的资源类文件。最初有品的版本包的大小只有800K左右,但随着业务的增加版本包的大小也逐渐增加到3M,在当时网络处于3G到4G转换的时期,这个体量要做到及时更新就需要使用某种技术方案来优化更新速度。

针对早期的单包构建和加载方案,使用Diff-Match-Patch则是最直接有效的优化包更新速度的方式,服务端拿最新的版本包和上一个版本包做代码Diff并生成一份儿Patch包,这个包Patch包的体量相较于整包的大小是非常小的,往往仅有几K或者几十K的大小,客户端则通过拿到本地包的e-tag去访问更新接口,来获取适用本地版本的Patch包,最后客户端拿到Patch包结合本地缓存的版本包进行Patch操作就可以达到快速更新的要求。按照当时使用经验服务端只需要保留最后五个版本的Patch包就可以覆盖90%以上的更新用户。

图片

解决更新慢的问题后另一个可以提升页面加载速度的就是在客户端做版本包缓存,Diff-Match-Patch优化仅仅解决了版本包下载链路的问题,还有一些初装App或者许久未打开App的场景,这就需要在客户端每次发版时内置一个当前发版时间的最新版本包,并在App启动时优先加载本地缓存的包或者内置包,这样可以保证用户可以提早看到具体的业务页面,然后客户端在后台同步进行最新版本包的检测和下载,待用户切换至无React Native页面时重新加载最新的版本包,达到无感知的版本更新与重载,当用户再次打开React Native页面时看到的就是最新版本的页面。

图片

后期随着业务的发展,单包的体积愈发不受控制,仅某单一业务的源码和资源都可达到1~2M,在App内可能有十几个业务,这种情况就需要考虑进行包拆分来解决,有品通过调研并设计适用于自身业务场景的拆包方案来适应有品业务的扩张,后面会通过其他文章介绍方案细节。

资源处理

React Native构建输出产物中,我们用到的不仅仅是编译后的可执行JS Bundle文件,还有一些资源文件供业务加载使用,比如图片(PNG、JPEG、SVG)、JSON文件、表格、PDF等等。如下图:

图片

想要正确加载到这些资源文件,基本方式就是获取这些资源在本地的存储路径,然后就可以正确加载到这些资源文件。React Native的打包工具Metro为我们提供了一套解决require方式引入的本地资源文件资源加载方案,对应Android系统,其基本原理就是Metro打包时会把require中的文件按照文件路径去掉'/'符号并替换为'_',将路径前追加scale对应的dpi drawable路径输出到构建产物,并把该资源文件生成一个资源模块打到包内,比如构建完成的一个资源模块如下:

图片

这个资源文件在文件系统内的存储路径就是drawable-mdpi/testapp_youpin.png,原生端加载该模块时通过SourceCode模块持有的scriptURL获取当前包的绝对路径然后根据模块的参数信息抹去'assets',转换时也同时转换为实际的文件名格式并追加到scriptURL后面,最后得到文件在文件系统内的真实路径完成加载。

抛去React Native原生支持的这种资源加载模式我们还可以自定义图片加载,大概思路就是在构建完成Bundle文件后把需要图片文件夹拷贝到和Bundle同级目录内然后压缩下发给客户端,客户端再提供一个原生模块来获取当前资源包在文件系统内的绝对路径,最后JS调用原生桥接的获取Bundle存储路径的方法去获取到这个绝对路径并拼接资源文件的相对路径完成图片资源的加载。示例代码如下:

图片

WebView

有品的业务场景中也会使用到WebView来加载一些三方的H5页面,这类页面加载到小米有品App内并依赖有品的登录态以及涉及到调用有品App内的提供的API进行H5与原生的通信。React Native社区中中提供的react-native-webview库可以支持并提供WebView容器进行H5页面的加载,此外,在我们使用该库的时候通常需要解决Cookie同步和Native通信问题。

Cookie同步

在Android系统中加载H5页面时需要使用CookieSyncManager可以把当前域名的Cookie信息在加载WebView之前同步到WebView的JSContext中。

图片

在iOS系统中,由于UIWebView支持自动同步Cookie,所以使用UIViewView组件则不需要处理Cookie问题,但迫于内存和性能方面的考量,建议统一使用性能更高内存占用更小的WKWebView来加载H5。Cookie同步时,在WKWebView加载URL之前先判断域名下的Cookie是否已经同步,如果没有同步则通过在WKWebView内部注入JS脚本,该脚本待整个doucument加载完成后通过document.cookie = xxx的方式把Cookie注入到WKWebView的JSContext中。

图片

WebView通信

WebView中的H5与React Native代码进行通信的处理方式大致和原生端WebView与原生通信处理方式类似,有两种方式来支持H5调用React Native端提供的方法,第一种是通过WebView控件提供的onShouldStartLoadWithRequest回调方式进行URL拦截,双方约定一个URL格式用来进行通信,React Native端拦截到符合格式的请求链接,然后阻断请求并获取链接当中的方法名称和方法参数进行方法调用。这种通信方式能够有效的实现H5调用React Native或者Native提供的函数,但是由于URL的链接长度限制,不能很好的处理二进制文件,特别是一些大文件数据的传递操作;另一种方式是向WebView的JS上下文内直接注入关联对象,让H5内的JS可以直接调用注入对象的方法达到方法调用和回调处理。Native或者React Native调用H5方法则相对容易,可以获取WebView对象的当前引用调用它的postMessage方法,H5端监听消息队列来进行通信。

图片

Web适配

React Native目前官方仅支持iOS和Android两端,想要在不改动React Native源码的情况下让同一套业务代码能够在浏览器上运行对于研发同学来说是非常重要的,支持输出Web端可以减少大量工作量,真正实现一份代码三端复用。React Native在iOS和Android端使用原生组件进行渲染,在浏览器当中只需要把渲染部分使用浏览器支持的DOM进行渲染就可以实现React Native在Web上的转化,目前比较成熟的方案就是React Native官方推荐的社区开源库react-native-web来解决,使用方法简单方便。

传送门:  necolas.github.io/react-nativ…

动态化

React Native为我们提供了动态更新的支持,可以在不更新App客户端的情况下发布升级新版业务功能,但有时我们并不满足于当下,我们想要更快的更新速度,甚至不用业务包发版的情况下达到业务的更新。快速更新在某些场景下是很有必要的,特别是微小的UI改动,不断新增的UI楼层样式以及复用性较高的组件,所以要做到Low-Code/No-Code,追求动态化的极致。小米有品内部使用的Teris就是用于实现理想的动态化方案,他根据现有业务需求,抽象基础组件并定义一套标准的数据规范,通过解析该数据规范创建节点并渲染页面。

图片

原生组件&UI库

React Native的一个特别重要的优势就是它的社区活跃,全世界的开发者都在帮你实现某些通用的组件库,可以为我们节约不少研发成本,所以站在巨人的肩膀上才能看的更远。常用的三方组件库有提供网络状态监测的@react-native-community/netinfo、支持相机能力的@react-native-community/cameraroll、进度条组件@react-native-community/progress-view、横向滑动组件@react-native-community/slider、支持地理位置信息的@react-native-community/geolocation等等。

对于不满足我们需求的组件,我们还可以自定义去实现,小米有品大前端团队也根据自身需求的特殊性实现了内部使用的通用类UI组件和业务类组件,比如商品卡片、地图、嵌入式视图、直播组件等等。

图片

                                           通用跨端组件库Duplo

    

性能

高性能是小米有品App选择React Native技术方案的重要因素之一,借助于原生实现节点渲染使得页面的流畅性完全可以和原生页面相媲美。如果你是一位客户端开发者,你在实现一个页面的时候肯定会考虑这个页面平均帧率,页面的内存占用,页面中动画如何实现才能减少CPU的计算来减少电量的消耗。而当前端开发者使用JS去开发React Native页面时往往不知道或者很少去关注这些原生端实现界面开发时关心的优化点,结果就是页面出现卡顿、内存爆炸、动画不流畅。使用React Native开发时重点关注长列表的性能优化、内存优化以及动画这三点基本上可以解决90%的性能问题。

长列表

长列表在前端页面开发中应用的最多,在浏览器开发中大部分使用块级元素进行平铺就可以实现长列表展示,但在React Native中这种简单的流式平铺往往会造成页面的卡顿。React Native为我们提供ScrollView、VirtualizedList、FlatList、SectionList四种官方组件来实现长列表的瀑布流,其中FlatList和SectionList都是基于VirtualizedList的上层封装,VirtualizedList相较于ScrollView提供了元素复用机制,可以有效的减少内存占用并提高页面滚动流畅度,同时也提供部分配置项来优化列表渲染,比如以下配置项:

  • initialNumToRender 可以指定开始渲染元素的数量,提高首屏渲染速度,使用该配置时尽量配置元素数量刚好填满一屏,因为这部分元素在用户滑动时不会被卸载;

  • keyExtractor此函数用于为给定的元素生成一个不重复的 Key。Key 的作用是使 React 能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。

  • getItemLayout是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。

  • removeClippedSubviews 一个将“剪裁子视图”(clipped subviews)(指的是那些在父视图之外的视图)从视图层级中删除的本地优化,为的是减轻渲染系统的工作负担。但是这些被剪裁掉的子视图依然保留在内存中,所以它们所占的储存空间没有被释放,内部状态也都保留了下来。这可能会极大的改善长列表的滑动性能

  • windowSize 设置可视区外最大能被渲染的元素的数量,以可视区的长度为单位。比如说,如果列表占满了整个屏幕,而 windowSize 属性被设置为 21 的话,那渲染的长度为包括当前可见屏幕区域在内,往上 10 个屏幕的长度和往下 10 个屏幕的长度。将 windowSize 设置为一个较小值,能有减小内存消耗并提高性能,但是当你快速滚动列表时,遇到尚未渲染的内容的几率会增大,而这些尚未渲染的内容会暂时性地被空白区块所替代。

  • maxToRenderPerBatch 每批增量渲染可渲染的最大数量。能立即渲染出的元素数量越多,填充速率就越快,但是响应性可能会有一些损失,因为每个被渲染的元素都可能参与或干扰对按钮点击事件或其他事件的响应。

  • updateCellsBatchingPeriod 具有较低渲染优先级的元素(比如那些离屏幕相当远的元素)的渲染批次之间的时间间隔。与 maxToRenderPerBatch 具有相同的目的,都是为了在渲染速率和响应性之间获得一个平衡。

内存

内存占用一个也是React Native开发中影响性能的重要因素,开发者必须时时刻刻考虑到如何去降低内存的占用,虽说目前手机内存大部分已经达到6G,甚至12G,但是过高的内置占用容易使处于后台的App被系统进程杀死。

图片

常见的内存优化也比较多,比如减小Bundle包的大小。Bundle包被加载到内存当中是常驻于内存的,可以通过提取公共组件减少重复代码,从而缩小Bundle包体积。运行时JS代码中高内存占用的变量要及时释放来减少内存消耗,另外也可以使用RAM格式的Bundle延缓加载未使用的模块,或者对Bundle进行拆包处理实现按需加载并释放不使用的业务代码从而减少内存占用;上面讲到的长列表优化当中使用可复用的瀑布流组件也可以减少当前内存占用;另一个内存占用的大户就是图片,一个页面当中使用过多的图片会造成内存的暴增,特别是显示区域较小的图层加载高分辨率图片,固定的图片显示区域使用相同大小的图片可以大大减小内存的占用。虽然图片的大小不影响最终绘制的图形大小,但高质量图片需要被读取并缓存到内存当中而导致内存占用上升,另外也可以使用支持内存缓存管理的图片组件,比如FastImage, 也可以用SDWebImage进行自定义封装等等。图片色彩饱和度在一定程度上也会增加内存的占用,也可以通过调整图片色彩饱和度来降低内存占用;其他造成内存占用过高的因素还有组件的View层级太深,虽说React Native在实际渲染时已经自动优化掉无用的View层级,但是我们需要在开发时就关注并减少View层级数,对于层级较深的视图层可以考虑绘制成图片来减少内存的占用。

动画

我们知道,根据React Native的实现原理,想要改变一个UI的样式是需要经过一次JS和Native通信来实现,这很大的限制了动画的性能。React Native中为我们提供了Animated和LayoutAnimation用于动画的实现,动画的性能上我们可以通过启用useNativeDriver来使用原生动画驱动,这样可以跳过每帧绘制时的JS线程和UI线程通信达到性能的提升;另一种是使用setNativeProps来直接操作原生DOM,减少大量setState造成的JS线程内的Diff计算;React Native也为我们提供了InteractionManager模块,可以将一些耗时任务安排到动画完成之后再执行,增加动画的流畅度;最后我们还可以借助一些第三方库来实现动画,比如BindingX,有兴趣的可以尝试一下。

重绘

过渡的重绘也会带来性能的损耗,所有的JS计算目前都在同一个线程中进行,线程阻塞就会影响交互的流畅度,我们可通过shouldComponentUpdate来减少不必要的重绘,可以借助PureComponent组件来替我们完成这部分的优化;避免在设置props属性时直接使用内联箭头函数也能减少组件的重绘,内联箭头函数在每次绘制时都会生成一个新的对象,相当于对props设置了一个新的值导致重绘;在React Native中也提供了setNativeProps方法来直接操作DOM,所以小的局部变化可以借助setNativeProps来提升性能;最后一些图层的圆角、图层的阴影和透明度虽然不会引起重绘,但会造成GPU的过渡绘制而影响帧率。

拆包

一个前端项目主要流程就是开发、打包、部署,其中打包过程中一个优化点就是将大包拆分成小包,优化后能够有效的降低内存的占用,并且减少加载的时间以及减少网络流量的消耗。早期React Native在有品是采用单Bridge单Bundle的模式进行使用的,如果使用React Native来支持导购、会场、搜索、众筹等这些单业务单Bundle的业务场景时就需要针对每个业务进行单独打包,受限于React Native基础JS代码的量以及移动端设备的性能,特别是内存方面。商城类业务需要业务间页面来回跳转,页面堆栈很可能包含多个业务页面,而且Bundle加载初始化时间长影响页面打开速度,所以目前不足以支持单Bundle单Bridge的模式,需要使用多业务单Bridge多Bundle的模式,对业务包进行包拆分,提取公共基础模块实现内存共享。

图片

有品结合自身业务特点设计并开发了支持RN拆包的系统平台,该平台共有四大模块:拆包平台管理前端负责应用管理、业务管理、包管理等前端操作;包管理系统负责提供管理平台基础服务、包的分发、对接打包服务、对接权限系统和流程管理系统等;打包平台提供Bundle的打包能力,对接云存储和Sentry平台等;客户端SDK则提供容器展示、包更新和加载策略、日志上报等功能。

结束语

本篇文章简要的介绍了React Native技术在小米有有品App中的应用,并未对应用的技术细节做深入讲解,加上React Native技术点又非常多,所以后续会更新一系列与React Native技术相关的文章来为大家做深入介绍。本篇先作为React Native技术专题的开篇,让大家对React Native技术有个初步了解,如果大家对React Native技术有兴趣,可以关注我们的后续更新。

招聘

小米有品是小米旗下的一个精品电商平台,有品大前端团队也在专注打造一个专业的电商前端团队,欢迎关注前端机会的同学联系:[email protected]

猜你喜欢

转载自juejin.im/post/7041376726030483493
今日推荐