作者:苑永志
作者介绍:现任特赞大前端负责人。技术涉猎比较广泛,曾在大麦网担任高级Java研发工程师;后以前端工程师身份加入特赞,基于React技术栈构建开发前端项目,并使用React Native开发特赞移动APP;目前正在使用Node.js开发和维护特赞服务网关,希望Node.js能够在更轻量级的微服务架构中发挥重要作用。
一、需求缘起
特赞是在2016年末才开始着手APP开发的。记得那是距离过年还有一个月的时候,产品突然提出一个需求:咱们做一个iOS应用吧,快过年了,给设计师一个新年礼物。其实当时我的内心是拒绝的,却也没办法只能面带微笑说:“好啊,我们尽量吧…”iOS工程师是别指望了,既然是大前端,那就什么都得做。
于是我们开始调研苹果应用的审核发布流程、热更新,以及具体的实现细节。为了赶上苹果的审核进度,两周时间我们发布了第一个初始版本,然后在接下来的两周时间完成了剩余所有功能的开发,并通过热更新完成线上发布。也就是说,我们前后用了不到一个月的时间,完成了特赞原生iOS APP的开发。
APP包括项目列表页、项目列表、报价列表、报价详情、对话页等。如图所示是对话页,设计师和客户可以进行实时沟通,这也是我们APP的一个亮点功能。
二、APP开发技术的选型
前端的同学都知道,使用React的话,是通过声明式的方式定义组件,然后通过虚拟DOM在浏览器环境下进行UI的渲染和数据的加载。React已经应用到了PC页面、移动页面,甚至服务端渲染等场景,随着React Native的推出,前端同学更是通过React拥有了开发iOS和Android应用的能力。
这真的是原生应用!就像 React的官方slogan:Learn once, write anywhere。翻译成中文就是:“一次学习,到处挖坑”。 先挖好坑,至于谁去填,这就是后话了 : )
为什么选择React Native呢?
首先,上文提到,通过React Native开发的应用,只要优化得当,几乎可以无限接近Native应用的交互操作体验。丝滑的手感,让人爱不释手。
其次,React Native开发出来的应用,其功能和性能都很强。
第三,React Native可以直接通过Chrome进行调试,这对前端开发人员来说简直就是一个福音。
第四,我们团队本身使用的就是React技术栈,所以React Native是一个很自然的选择,适应的过程也非常短。
最后,React Native除了可以像WEB一样进行开发,还拥有WEB一样的发布能力,只要通过热更新就可以简单做到。这一点也是影响我们抉择的一个因素。
三、React Native开发过程中的主要问题
本次分享主要围绕React Native开发前后涉及到的方方面面进行探讨。包括
- 开发前我们会重点考虑的调试、路由、数据管理、组件选型等问题;
- 开发过程中,要解决的动画、缓存、手势、支付等问题;
- 业务功能开发完毕后,要关注的消息推送、异常监控、热更新、性能消化等话题。
3.1 调试
如图,当调试工作能够通过Chrome的DevTools进行时,一切似乎都变得简单起来了。我们可以进行熟悉的断点调试、变量审查;还可以结合React、Redux的Chrome插件直观地查看组件结构和整个工程的数据变化。
这里有两个坑值得一提:
- 一是一旦应用live reload之后,断点调试就会失效,要重新reload应用才能恢复;
- 二是调试过程中保证DevTools所在TAB在最前面,否则APP会瞬间变得卡顿。
3.2 路由
React在WEB上可以通过react-router来管理路由,而在React Native中,路由管理变得更简单。利用Navigator组件,我们把所有的Scene、场景或页面通过一个堆栈管理起来,页面的操作就是简单的出栈入栈操作。
比如我们最初处于home页,接着push到对话列表页,再push到对话详情、项目列表页,然后又可以pop回对话详情页。当然实际情况可能还要复杂一点。比如往回跳多个页面、跳回指定页面等,这一切都是针对一个堆栈来进行操作的,所有这一切都可以用类似下图的这一行代码来实现。
通过Navigator组件对象的引用,我们可以跳转到对话列表(chat)页面,与此同时,我们带上项目ID、设计师ID等参数,这些参数在chat页面中很容易获得。
3.3 数据管理
使用React做过Web开发的同学知道,我们往往通过Redux把数据集中管理起来。在React Native中不同的点在于:我们希望数据能够被持久化,以免每次应用重启之后,所有数据又要重新加载。AsyncStorage能够很方便地跟Redux集成到一起,下文会介绍AsyncStorage的具体应用。
3.4 组件选型
组件也是我们是否选择一个前端框架的重要考虑因素。React Native框架本身给我们提供了很多实用的组件,比如列表、触摸操作、导航、图片等,但这些还远远不够满足我们的使用需求,所幸的是React Native社区非常活跃,有很多组件可供选择和直接使用,比如轮播、侧滑、文件上传……
3.5 动画
以上四部分都是一些准备调研,真正的挑战才刚刚开始。
如图所示是一个报价列表的页面,在使用中动画效果没有卡顿和掉帧的现象。在对话列表页内部,还可以通过上滑直接将列表中的一项放大成全屏,继续上滑TabBar还可以置顶,并且可以进行滑动切换操作,下滑又可以退出全屏。
在WEB页面中,我们通过CSS3动画(比如transition、animation等),可以方便地实现很多过渡和动画效果。JS层面,我们也可以使用requestAnimationFrame来进行动画操作,实际上很多动画库就是基于它来进行封装的。在React Native中没办法通过过渡、动画帧来实现动画,但React Native框架给我们提供了更为精细的动画支持。如下图所示:
Animated组件能够用于实现精细体验、友好的交互动画。我们可以通过定义特定的Value作为动画变化的参数,而这些动画可以是随时间渐变、弹跳或者有加速度的。动画可以是单个的,也可以是多个通过并行、顺序、交错的方式进行组合的。
这些动画都必须应用在特定的动画组件之上,除了内置一些动画组件,我们还可以根据需要自定义动画组件。在动画的过程中,我们还可以进行跟踪,根据Animated.event对象获得动画过程中的相关变量。
实际上,上文提到的这些用以实现一些常规的动画效果已经绰绰有余了。下面我们通过几个代码片段,直观地感受一下。
首先我们定义一个动画的值opacityValue用于记录透明度的变化,然后将这个值应用于Animated.Image组件的style属性之上,这跟我们书写内联样式没有什么区别,只不过opacity的值是我们定义的特定类型的动画值。
那我们如何触发这个图片的透明度动画呢?
我们使用Animated.timing对透明度进行一个线性的操作,第一个参数是我们定义的值,第二个参数是指定动画完结时的值,持续时间,变化虚线。
综合起来就是说,在4秒钟之内,图片的透明度将会由0线性变化成1。
在动画完成之后,我们还可以在回调中做一些事情。这是一个很有用的操作,我们可以把动画和业务操作错开来,避免动画和数据操作同时占用资源,造成卡顿。
除了上文介绍的精细控制,React Native也提供了粗粒度的动画控制 LayoutAnimation,我们可以把多个动画值一次变化到另一个状态,具体的动画效果交由框架去完成。简单的动画可以这么做,但是一旦复杂起来,我们还是会更加倾向于使用Animated去做控制。
React Native的组件还为我们提供了一个很有意思的接口:setNativeProps,顾名思义就是设置原生组件的属性,类似于我们在WEB中直接操作DOM。结合requestAnimationFrame会有意想不到的惊喜,但一般情况下我们不建议大家这么使用,因为脱离了框架的操作会让程序变得失控,除非你自己知道自己在干什么,还有别忘了写上显著的注释!
3.6 手势
一般情况下,动画都是伴随着手势产生的。React Native中很多组件都对手势操作进行了封装。比如Touch打头的组件,对触摸操作进行了处理,ScrollView中的onScroll对滑动操作进行了封装。
React Native中的事件也是分为捕获、目标和冒泡三个阶段,在各个阶段都可以进行一些操作,比如判断是否需要响应事件,我们可以在子组件之前响应事件,也可以在子组件之后响应事件;还可以处理简单的触摸事件和滑动操作。
有的情况下,我们需要把多个手指的操作协调成一个单点操作,这时我们需要使用PanResponser。这与前文提到的事件处理非常类似,因此不再赘述。
通过一个例子直观感受手势操作的处理。
我们不区分单个或多个手指,因此我们使用PanRespnsor来处理,在onMoveShouldSetPanResponser中,我们判断手指(可能是一个,也可能是多个) 滑动时,组件是否需要响应该手势。
接下来是一些业务判断,比如正在加载数据、水平方向上有滚动、正处于加载完毕提示页、水平位移大于竖直位移时,不需要处理;如果是全屏,向下滑动时,需要响应;如果是列表状态,向上滑动需要响应。当然,这仅仅是手势响应的一部分,还需要其他很多配合才能将手势和动画组合起来。
3.7 缓存
前文提到AsyncStore可以和Redux配合起来使用,实际上,所有需要进行持久化缓存的数据都可以使用AsyncStorage来进行操作。
为什么是AsyncStorage?虽然localStorage也能进行持久化缓存,但它的接口是同步的,在JS单线程模型中,耗时的阻塞IO操作令人非常郁闷。所以AsyncStorage正是为了取代localStorage而出现的,使用异步在JS中是最佳实践。
不过,我们一般不建议直接使用AsyncStorage,因为它存储的内容都是字符串,每次操作的时候都要进行序列化、反序列化,同时还要捕捉异常。所以我们通常把存取操作封装起来,如果有必要,也可以加上命名空间来区分不同的数据资源。
APP中,除了数据缓存以外,图片等资源的还原也显得格外重要,用户不希望一个10M的APP在多次使用后莫名其妙地变成了1个G,占用了用户的内存,也浪费了“不菲”的流量。对于图中这样地址不会变更的图片,我们只要使用一个图片组件就可以将图片资源缓存起来,避免重复下载。
对于七牛资源这种动态变化的地址,刚才的方案就不可行了。为了保证设计师资源的安全性,我们每次给到客户的资源都是一个带有token标识的链接,而这个链接很快就会过期,这就意味着同样一张图片也会重复进行下载,这是一件很恐怖的事情。不过我们看到每个七牛资源的key是不变的,比如图中的“5483389ab...”部分,那就可以利用这个key去判断是需要重新下载,还是可以从文件系统中直接读取该图片。
具体的实现,我们用到了react-native-fetch-blob组件,它可以配置资源下载后的存储路径,然后根据这个路径从本地文件系统中直接读取到该图片,从而实现了对非固定路径资源的缓存。
3.8 支付
为了实现资金的闭环,支付是必不可少的一个功能。在Web端,我们使用Ping++的支付服务,当我们知道Ping++没有React Native的SDK时吓了一跳,万幸的是他们内部正在研发React Native的SDK,在经历了各种坎坷之后我们的第一笔钱终于付出去了。
有了开发前的准备工作,也攻克了开发过程中的种种困难,一个APP的开发工作就基本完成了,可就在你以为大功告成的时候,却发现它竟然会崩溃、卡顿,而且还不容易定位到原因。因此在业务功能开发完毕后,还要关注消息推送、异常监控、热更新、性能消化等话题。
3.9 消息推送
消息推送是必备功能,其流程比较简单。如果是iOS应用,首先我们需要使用苹果开发者账号申请一张证书,iOS APP可以获得设备token, 后台结合token和证书可以申请消息推送的请求,获得授权之后,就可以调用推送接口,直接推送必要的消息既可。
3.10 异常监控
在开始讨论异常监控之前,我们最好先了解异常发生的原因。上图是React Native在Android和iOS两个平台上的架构,最上层是打出的安装包,可以运行在对应的操作系统上。中间部分是我们书写的JS代码,再往下是原生组件和核心类库部分。
以上,我们可以大致看出可能的异常来源:
- 用户业务代码产生的异常,这部分属于JS异常;
- JS模块和Native相互调用可能产生的异常,我们称之为Native异常;
- 还有组件渲染过程中产生的异常,这部分叫做UI异常。
我们分别来看各自异常的处理方式。
全局的JS异常可以通过react-native模块中的ErrorUtils工具类来捕获,也可以通过模块react-native-exception-handler来统一处理,比如记录的日志系统。
通过React Native Android框架的源代码,我们可以找到对应Native异常和UI异常的错误处理,这就意味着我们可以通过修改源码来自定义异常的处理方式。不过一旦升级React Native版本,就需要做出相应的修改,这会带来维护成本。
如果你认为这些方法太复杂,那也可以使用像bugly这样的异常监控平台,它不仅可以随时监控APP的崩溃、卡顿和错误等发生的情况,还可以清晰地知道用户和手机的分布情况。
3.11 热更新
JS可以通过应用内的JS引擎动态解释执行。所以无论源代码做了多大的修改,只要无需构建,我们都可以通过热更新动态推送到用户的手机上。这个过程大致如下:当用户的APP启动或唤醒的时候,检查APP内的bundle和图片资源是否是最新的,如果不是,则从热更新服务器加载最新的bundle和图片。实际情况可能要稍微复杂一点。
在用户的APP这边,我们需要检查bundle版本,进行下载、解压、reload操作。如果是增量更新,还要进行bundle合并。对于热更新服务器,我们要针对Android和iOS提供不同的bundle版本,每次发布或者更新时都需要打包发布对应的bundle版本;如果是增量提供bundle patch版本,还要对bundle进行拆分。
所有这些都做完的工作量可能甚至要超过APP开发本身的工作量了,那有没有现成的解决方案呢?
解决方案显然是有的。我们目前使用的是微软提供CodePush热更新服务,只要简单注册配置,然后在APP端引入CodePush的客户端插件,就可以完成上文提到的相关工作。它还提供了版本的出错回退机制。不过访问速度是它的缺点,如果每次更新需要几十M甚至上百M,那就要斟酌一下了,目前来看,我们使用起来感觉还是很好的。
3.12 性能优化
最后也是最重要的一块内容是性能优化。所谓天下武功,唯快不破,如何让我们的应用快起来是性能优化的关键。下文将从加速速度、滚动速度和响应速度三个方面来提供一些优化建议。
3.12.1 加速速度
加速速度带给用户的是既视体验,如何避免白屏、把数据和页面第一时间呈现给用户是关键。首先我们考虑的是从缓存中加载往次访问数据,然后异步加载最新的数据。如果是对实时性要求并不是很高的数据,我们可以使用Redux中统一管理的数据,前文提到过,这一部分数据我们也做了持久化缓存。
3.12.2 滚动速度
大列表是在应用中必会出现的部分,而列表本身操作又特别复杂和频繁,这就导致列表内的组件会重复渲染,从而带来极大的性能消耗。通过使用shouldComponentUpdate可以判断组件是否需要渲染,从而阻止不必要的渲染——这很简单,也非常有效。
除此之外,ListView列表组件本身也提供了一些配置来提高渲染效率,比如首屏加载的数量、可视部分的数量。如果你刚刚开始使用React Native,恭喜你,你可以使用FlatList组件,列表的操作变得简单,性能也非常出色。
3.12.3 响应速度
如何提升响应速度?在Navigator页面切换后,如果需要通过网络加载数据,很容易造成转场动画的卡顿,这是因为业务逻辑和UI渲染逻辑出现了交错。
React Native提供了InteractionManager帮助我们去处理这样的情况,只要把业务逻辑放到runAfterInteractions方法的回调中去执行就可以确保转场动画的完整展示。按钮点击或其他的一些操作可能也会出现类似情况,处理的方式也是大同小异,requestAnimationFrame的回调使得交互和业务逻辑能够错开。实际上,卡顿丢帧的始作俑者都是JS单线程,如果使用setNativeProps就可以跳出这个模型。但还是那句话,除非你知道自己在做什么,否则不要这么做。
当然还会有很多其他的优化建议,没有办法在本文中完整列举下来。实际上,如果你按照上面给出的建议去做了优化,APP的体验应该已经很不错了。但凡事无绝对,实在快不起来的时候,别忘了把Loading效果用起来,这会让用户愿意多等一会。
原文发布时间为:2017-09-07
本文作者: 苑永志
本文来自云栖社区合作伙伴“中生代技术”,了解相关信息可以关注“中生代技术”微信公众号