携程火车票业务在 React Native 实践中踩过的坑

前端之家收集整理的这篇文章主要介绍了携程火车票业务在 React Native 实践中踩过的坑前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

本文为携程技术中心投稿,版权归原作者所有,非经允许,请勿转载。原文是禁止转载的,此次转载用于学习,非商业使用。

原文链接:http://geek.csdn.net/news/detail/114527

【作者简介】姚瑞琼,前端程序媛一枚。2014年毕业后加入携程火车票事业部,今年年初起至今,主要负责 React Native 方案在火车票业务线的实践,先后参与并负责汽车票 RN 独立版、携程 App 抢票 RN 版的开发迭代。
欢迎技术投稿、约稿,给文章纠错,请发送邮件mobile@csdn.net

火车票作为携程体系下的重要环节,要兼顾良好的 App 用户体验及迅速的业务迭代,一个月左右一次 App 版本的节奏很难满足,而 React Native 跨平台、媲美原生 App 的用户体验以及无需发版的升级模式等等优势无疑使人眼前一亮。

加上基础团队的 Ctrip React Native 框架对 RN 的性能优化、业务封装以及拆包发布等的大力支持,火车票现已上线将近 20 个 RN 页面,经历了携程 App 三个大版本的迭代与考验。

本文将着重介绍 React Native 在携程火车票产品中的应用,以及在 RN 实践过程中遇到过的一些实际问题与解决方案。

本文大致分以下几部分内容

  1. 为什么选择 React Native
  2. React Native 的现状
  3. Ctrip React Native
  4. 携程火车票的 RN 应用
  5. 踩过的坑及解决方
  6. 各种问题及优化步骤

一、为什么携程火车票要选择 React Native

作为目前携程 App 为数不多的主要以原生开发方式为主的 BU,我们也曾在 Native 跟 Hybrid 两种方案中纠结过。一方面,原生的交互性能用户体验都是最优的,Hybrid 则始终还未能突破性能瓶颈;但是另一方面,原生应用的更新又是一个很大的问题,尤其是 iOS 的 App Store,每次发布更新都需要漫长的审核周期,无法做到及时更新,并且上线之后的维护也非常麻烦。

对于业务高速发展、更新比较频繁的火车票业务来说,携程 App 一个月左右一次的大版本发布已然无法满足需求。所以这个时候,基本兼顾到体验与更新两方面优势的 React Native 的出现,无疑非常值得我们一试。

性能体验来说,Native 最为优秀,RN 基本接近,Hybrid 则有些硬伤,因为 Hybrid 的 View 层实际上还是前端开发人员所熟知的 DOM,而 React Native 则是以 Virtual DOM 的方式操作跟渲染相应平台的 UI 组件,所以性能要高于 Hybrid 而不逊色于原生;而更新复杂度上,Hybrid 跟 RN 都比较低,可以进行无需发版的 bundle 包更新,而 Native 则受限于应用商店的发布更新,复杂度最高;另外,从开发成本角度来看,Native 开发周期相对较长,编译调试也相对复杂,并且不能跨平台,Android 跟 iOS 需要维护两套完全不同的代码,RN 虽然比 Hybrid 成本稍高,但是远小于 Native,可以做到大部分代码的跨平台复用。

二、React Native 的现状

从这上述的几个方面来看,RN 的各项表现都是非常优秀的。然而,目前 React Native 仍以每两周一个版本的更新频率快速变化中,到现在最新的 0.35,仍旧是以零点几的版本在定义,还不能算是一个完全成熟的框架,所以在实际应用过程中还有许多坑要趟。比如版本升级问题,有时候可以平滑过渡,有时候就没那么轻松了。

就拿我们年初实践的汽车票独立版来说,1 月份刚开始使用的时候,React Native 刚开源 Android 版本不久,在 Android 上的兼容性还不是很乐观,所以只在 iOS 上做了尝试,我们最开始使用的是 0.18 的 RN 版本,从如何集成到现有的 App 里、怎么打全量包或增量包、以及 bundle 包的发布等等问题,当时都是组里的小伙伴跟 iOS 开发小伙伴自己一步步摸索过来的,但是在 RN 的快速更迭下,等尝试升级到零点二几的RN版本时,一言不合某些组件跟 API 就不能用了。

三、Ctrip React Native

在携程基础团队向我们各个业务团队提出 Ctrip React Native 的支持时,我们几乎毫不犹豫就确定要在携程火车票里接入了,算是公司里 RN 应用比较早的 BU,也是第一个使用 RN 进行完整的购票下单流程的 BU。

CRN 抹平了很多 iOS 跟 Android 组件的差距,比如 DatePicker、SegmentedControl,提供各种携程风格的组件和API,如 HeaderView、HtmlText、Storage、Fetch 等,使业务开发更专注于业务逻辑,不需要过多费精力在跨平台的代码实现上,也避免了各个 BU 的大量重复劳动;

此外,CRN 对首屏渲染速度的提升,使 iOS 能在 200ms,Android 在 400ms 左右完成首屏渲染,以及对 ListView 的优化等都让 React Native 向 Native 靠近了一大步;另外,包括对打包拆包、业务代码发布及诸多工具比如 crn-cli 的提供和支持,都让业务方更无压力的接入 RN 应用。

可以说,CRN 实现了把 React Native 作为一个纯技术框架像业务框架的转变。

四、携程火车票的React Native应用

携程 App 从 6.17 版本开始有业务试用 React Native,到 6.18 也只有 2 个 BU 尝试了 3 个 RN 页面的上线。到 6.19 版本,火车票 RN 强势加盟,7 个 BU,21 个 RN 页面里,单火车票一家就占了 8 个。再到 6.20 版本,在 CRN 的加持下,15个 BU 接入了 50+ 的页面,我们到达了 17 个,包括云抢票、改签抢票、接送站等业务,并且这些页面大部分都比较重要且有较为复杂的交互逻辑。

五、踩过的坑及解决方

从一个火车票购票流程里粗略提取一下具体实现就有如下几点:

  1. 从几千个城市站点里选择目标城市;
  2. 在各种车次、座席、出发时间里筛选出合适的车次;
  3. 乘客信息的填写或者选择。

首先,购票流程第一步的站点选择,就遇到了一个大坑:大家应该知道全国大大小小的火车站至少有数千个。而接触过RN社区的小伙伴应该知道,React Native 的 GitHub 上有条臭名昭著的 issue,官方至今还没有完美的解决方案,就是 ListView 的性能问题。RN 自带的 ListView 是没有回收机制的,这样就使得 RN 在加载较多个数据的列表,App 会非常吃内存。

我们一开始也尝试用自带的 ListView 来加载城市站点列表,几千条纯 Text 渲染下来时感觉还能勉强接受,但在加上了 View 布局、Touchable 事件之后,当时连在 iPhone 6,iOS 8.2 的系统下也非常吃力,越滑越卡,甚至在较低配置的设备上还出现卡死甚至 Crash 的现象。

实际上,城市站点选择是一个变更频率很低但是使用频率很高的页面,考虑到 RN ListView 的优化空间有限,一旦出现卡死,对火车票来说,结果基本是灾难性的,所以我们最终选择了复用原生已有的城市选择页面,由封装成一个 station component 供 RN 使用。

现在我们考虑下另外一个重要场景的实现,从账号里的常用乘客列表里勾选乘车人,同样作为一个列表,是不是也可以像站点列表一样复用 native 组件呢?我们也确实这样考虑过,看起来好像省时省力、皆大欢喜。但其实正常情况下,个人账号里的乘客数量远小于上千条的站点数据,RN 实现是可以负荷下来的。而且 station component 只需要传给 Native 一个已选的车站,然后 Native 组件里操作完成后返回一个重新选择的车站就可以了。

乘客列表并非简单的单选,而且除了点选,还涉及到新增编辑等等更多的交互。由 Native 封装的话,涉及到的参数传值未免太多。那么是不是可以跳转到一个新的页面,加载跟渲染数量较少的乘客列表比较方便实现呢?

从产品层面来说,火车票购买作为一个购票流程,每多跳转一个页面就有可能损失一部分转化率,所以为了尽可能减少页面跳转,我们采用了浮层形式在订单填写页面里进行乘车人的选择。然后问题又来了,在浮层弹出的动画过程中加载并渲染乘客列表,很容易出现失帧卡顿的现象。如何解决

我们是这样考虑的,列表的加载并不是非要在浮层弹出的同时进行的,在进到订单填写页时就可以预先加载好乘客列表数据,而只在浮层里做渲染即可。而且可以在不影响用户视觉体验的前提下,增加一些短时间的延迟。先完成浮层的弹出动画,使用 RN InteractionManager 的 runAfterInteractions 等动画结束之后渲染数据,并且设置了 ListView 的 initialListSize 跟 pageSize 均为 1 来逐条进行渲染。

前面也提到了,我们是希望尽可能减少页面跳转的,所以像车次类型、时间段筛选、座席选择以及前面提到的乘客选择,都是在各个页面采用浮层形式来实现的。但其实浮层涉及到的问题相当多,这里仍旧以乘客选择浮层为例。乘客浮层要实现的功能非常之多,首先,内部的列表是可以滑动的,上部分的阴影可以点击消散浮层,并且内部的 Item 又要响应各种点击操作。

拿到这么一个复杂的需求,最开始的做法是先给整个 Modal 加个 TouchableHighlight 事件来处理消散动作,然后内层加上 TouchableWithoutFeedback 来避免触发外面的 hideModal 动作。

然后再在每个 Item 上加各自响应的 Touchable 事件响应勾选或者取消。然而,各种 Touchable 事件嵌套之后,实际效果就不在预期范围内了:滑动内层列表的时候突然划不动,点击 Item 却没有反应等等,经过一番调试跟定位,终于确定,ScrollView 滑动过程中很容易触发到外层的 TouchableWithoutFeedback,所以也就 without Feedback 了。

问题一定位,解决方案自然也就出来了,Touchable 的过多嵌套导致了问题的产生,那么就应该重新进行层级的布局,避免这些不应该的嵌套,不在整个Modal上加hideModal事件,而是抽出与浮层同级的View来做消散动作,内层只专注于List的渲染跟Item的点击事件监听。

六、各种问题及优化步骤

除了上述那些复杂的需求导致的复杂实现,还遇到过很多比较 low 的问题。大多数时候如果知道了一些属性就完全可以避免很多问题的产生。

不知道大家有没有遇到过 setState 方法刚设置完一个状态,取这个状态却发现没有生效的情况。这个异步方法让我写出过很多丑陋的 setTimeout 来尝试解决。结果查阅 React 文档后发现 setState 是有第二个参数的,这个参数就是设置完 state 之后需要立即调用函数

另外,合理使用 key 属性跟各种React生命周期钩子函数,如 shouldComponentUpdate,可以优化很多性能问题。

再比如长按累加累减这样的需求,单纯的 onPress 跟 onLongPress 是不能实现的,需要结合 delayLongPress 直接触发 onLongPress,并且在 onLongPress 里进行 setInterval 的递增或者递减操作,然后在 onPressOut 解除 press 动作的同时 clearInterval。

伴随着业务的高速发展,逻辑越来越复杂,我们也逐渐发现了很多做得不好,值得优化的地方。像前面提到过的 View 层级过多导致的内存消耗、页面卡顿。页面逻辑复杂导致 state 设置过多,出现切换卡顿现象,并且状态管理越来越混乱。而且现在很容易出现单个页面动不动就一两千,甚至几千行代码,维护起来非常困难,还有很多重复的代码实现等等。

这些问题我们也在考虑从很多方面优化,像布局上尽可能减少层级嵌套,尽可能抽取能够复用的组件,都是大家需要注意的点,状态管理上我们也在考虑如 Redux 等一些好的解决方案的引入。

以上,希望能与大家共勉。

猜你在找的React相关文章