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