React Native那些事
Posted clumsypanda
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Native那些事相关的知识,希望对你有一定的参考价值。
这几天在学习React Native(以下简称RN),阅读了一些文档和文章,本文是从RN是什么、为什么要使用RN、怎么使用(What、Why、How)的角度,整理的一些笔记和自己的看法(参考的文章链接已列在文章最后),本文侧重点是RN的基本概念。正处于入门阶段,望大家批评指正。
- WHAT—Learn once, write anywhere
1、RN的诞生
A、javascript的跨平台背景
从程序员的角度来说,很多是追求“Write once, run anywhere”的。1995年Sun公司将Oak语言改名为Java,正式向市场推出,“一次编写,到处运行”开始备受关注,网景(Netscape)动心了,它们在1994年刚刚发布了Navigator浏览器0.9版,亟需一门可以实现浏览器和网页互动的脚本语言,于是和Sun将Javascript联手推上市场。所以Javascript的命名也不是偶然,而其和跨平台也注定有不解之缘。
B、HybridAPP
在移动端飞速发展的现在,平台多样性虽然可以使得用户可以获得不一样的体验,但是其开发成本也逐渐变大,此处的权衡就显得尤为重要,于是很多人进行了创新与探索:Titanium、PhoneGap和Cordova等,尝试用纯前端开发技术来取代终端。它们使用网页进行布局和交互,以浏览器进行承载,同时提供一些额外的native交互方法便于完成特殊需求例如GPS、相机等。但是这样的结果是大量牺牲了用户体验,在交互、动画等方面与native app相比有很大的落后。
C、APP代码转化
既然APP套浏览器壳嵌网页的形式走不通,我们是不是可以尝试写一套工具,完成跨平台的代码转化?比如写一套ios应用,生成一个android应用。我能想到的,多半别人已经在做了。比如Apportable。这家公司已经在游戏的平台代码转化上有了很大的成果,但是毕竟游戏是以引擎为主,非游戏APP在不同平台之间的差异性决定了这条路还很漫长。
D、RN
“What we really want is the user experience of the native mobile platforms, combined with the developer experience we have when building with React on the web.”
伴随这样的口号,Facebook在React.js Conf 2015上推出了React Native,9月15日发布了React Native For Android,其想在用户体验和开发人员体验中做一个权衡。RN可以让开发者使用JavaScript和React进行组件化开发,也就是说RN给我们提供一个个封装好的组件让开发者来进行使用,甚至我们可以相关嵌套形成新的组件(目前RN在iOS上仅支持iOS7以上,Android仅支持Android4.1以上)。
2、RN基本原理
Java的“Write once, run anywhere”思路很棒,但对不同平台而言,系统和用户体验的差异性是至关重要的,也正是其存在的理由,所以过份要求应用在不同平台取得完全一致性是很不合适的。所以RN的理念是:“Learn once, write anywhere”,其推崇的是知识复用的概念,学习一种理念和语言进行跨平台应用开发。RN使用React作为用户界面构建的工具,并在此基础上重新包装了一个渲染引擎(不同平台可能不一致),可以将Virtual DOM生成不同平台下的UI,并创建了一系列的bridge来让js同native code进行通信。这样,就可以通过编写React代码来生成Native应用。RN提供了一种不一样的跨平台方式,其操作均是独立于UI主线程的异步执行,native主线程本身只做最重要的渲染与用户响应。
React:不同平台上编写基于React的代码,“Learn once, write anywhere”。
Virtual DOM:相对Browser环境下的DOM(文档对象模型)而言,Virtual DOM是DOM在内存中的一种轻量级表达方式(lightweight representation of the document),可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信。
Web/iOS/Android:不同平台的通信和展示。
MVC或是MVVM框架是常用的设计方案,JS作为统一的实现语言时,数据流和业务逻辑往往不是跨平台的关键,重点开始转为View层的处理,在这点上,简单概括React的思路如下图,开发人员主要控制Virtual DOM,ReactJS(RN)进行中间支持,解析成后续的浏览器DOM或者Native界面。所以我们不妨从Virtual DOM谈起。
3、Virtual DOM
当在实现一款APP时,最基本需要考虑的是往往是这些内容:数据层的设计、业务逻辑的实现、界面的展示,开发者代码描述和应用实际渲染结果的中间,RN添加了一层:Virtual DOM。这样做有个很大的好处:提高性能。代码直接修改浏览器DOM会是一个很大的开销,尤其在数据改变频繁时,甚至会影响用户体验。
RN的组件有状态的概念,可以在一定程度上记录当前组件的上下文。当组件的状态变化时,其相应的Virtual DOM可以根据要求及时更新,举个简单的例子:
class testreact2 extends Component {
constructor(props){
super(props);
this.state = {
fadeAnim: new Animated.Value(0),
};
}
如上图,我们定义一个组件testreact2,其中状态中记录了fadeAnim变量,以0初始化。在该View的样式上,我们定义其透明度为fadeAnim。
<Animated.View style={{opacity:this.state.fadeAnim}}>
在组件加载(下文会详细介绍)后,调用Animated.timing()方法对this.state.fadeAnim进行修改:第一个参数为需要修改的变量,第二个参数为配置。start timing方法,fadeAnim状态会进行变化,上面我们已经以这个状态来控制该View的透明度了,所以其也会变化,形成渐变动画。
componentDidMount(){
Animated.timing(
this.state.fadeAnim,
{toValue: 1}
).start();
}
现在我们知道了组件状态对Virtual DOM的管理,那么Virtual DOM的更新就容易理解了,根据状态的改变,RN判断Virtual DOM和Browser DOM的不同之处,找到所有影响节点,进行统一更新。值得注意的两点:一是Virtual DOM不是Facebook的优化项目,而是其React框架至关重要的思路,对数据更新频繁或者渲染要求很高的应有尤其有效;二是RN对Brower DOM进行内存上的缓存以便完成比较操作。
4、组件的加载和渲染
组件化是RN的核心思路,与Android的Activity和Fragment最大的不同是,它是可嵌套的。状态和属性是组件的基本描述,状态更多的是表示组件当前的情况;而属性描述的是组件的特性,也是最简单的跨组件通信单位。上小节已经介绍了组件状态和更新的基本框架,这里简单介绍下组件的加载和渲染流程:
A、绘制阶段
如图中的上面虚线框内,在这里完成了组件的加载和初始化。具体来说,首先使用getDefaultProps获取应用上下文,通过getInitialState得知初始化状态。然而开始正式加载:componentWillMount,因为此时还没有渲染,可以在此进行组件和变量的初始化操作。渲染完毕之后,在componentDidMount方法可以执行对组件的操作。这点和Android的Activity不同,其每个Fragment加载完毕的事件需要使用OnFragmentInteractionListener回调来避免crash。同时,网络请求的发起也可以在此进行,因为组件加载完毕且该方法只执行一次,是安全的。
B、交互阶段
如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面。componentDidMount之后就进入了组件的运行状态,组件的属性发生了变化,则进入componentWillReceiveProps回调,考虑到属性变化的结果,可以选择是否使用状态设置对组件状态进行更新。这里更新状态是安全的,不会直接触发render调用:
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
当组件的属性或者状态变化时,会使用shouldComponentUpdate进行判断是否组件需要重新渲染,如果需要,则进入渲染流程:componentWillUpdate->render->componentDidUpdate。默认情况下此处返回true来保证数据变化带来的同步更新,在做性能优化时可以考虑重载来自己处理。componentWillUpdate主要是对属性和状态值进行更新,更新完毕使用componentDidUpdate进行通知,此函数的输入参数变成了 prevProps 和 prevState。
C、消亡阶段
如图中右下角的虚线框中,这里做一些组件的清理工作。得到组件销亡消息主要使用componentWillUnmount进行处理,例如取消计时器、网络请求等。
5、手势系统
web app和原生app在体验上有巨大的差异的原因,facebook总结为两点:
Feedback/highlighting- show the user what is handling their touch, and what will happen when they release the gesture
Cancel-ability- when making an action, the user should be able to abort it mid-touch by dragging their finger away
http://facebook.github.io/react-native/docs/gesture-responder-system.html#content
即:
反馈/高亮——让用户看到他们到底按到了什么东西,以及松开手后会发生什么。
取消功能——当用户正在触摸操作时,应该是可以通过把手指移开来终止操作。
RN的事件响应流程如下图:
一个view 如果想要对事件作出响应,它只需要实现函数:
- View.props.onStartShouldSetResponder: (evt) => true, 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
- View.props.onMoveShouldSetResponder: (evt) => true, 如果View不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?
如果返回true,尝试要变成第一响应者,那么下面两个函数中的一个会被调用:
- View.props.onResponderGrant: (evt) => {} -View现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里。
-
View.props.onResponderReject: (evt) => {} - 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。
如果View已经开始响应触摸事件了,那么下列这些处理函数会被一一调用:
- View.props.onResponderMove: (evt) => {} - 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。
- View.props.onResponderRelease: (evt) => {} - 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。
- View.props.onResponderTerminationRequest: (evt) => true - 有其他组件请求接替响应者,当前的View是否“放权”?返回true的话则释放响应者权力。
- View.props.onResponderTerminate: (evt) => {} - 响应者权力已经交出。这可能是由于其他View通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如iOS上的控制中心或是通知中心)。
RN对事件响应系统有很好的封装,与Touch有关的有四个组件:
- TouchableHighlight
- TouchableNativeFeedback
- TouchableOpacity
- TouchableWithoutFeedback
四个组件分别表示:点击有高亮效果、Android内核的Native点击反馈(击区域中显示不同的效果,例如Material Design的点击波纹效果)、点击透明效果、不添加点击态。使用方法类似,举例如下:
<TouchableOpacity onPress={this._onPressButton}>
<View style={styles.topView}>
<Image source={require(‘./imgs/topBanner.png‘)} style={styles.topBanner}/>
<Image source={require(‘./imgs/xiaoren.png‘)} style={styles.xiaoren}/>
</View>
</TouchableOpacity>
相应的,我们需要实现_onPressButton方法:
_onPressButton(){
console.log("test");
}
可以看出来,这里的事件响应组件使用的是包裹的方法,和Native事件传递方式“隧道传递、冒泡处理”非常类似,我们知道嵌套层次更深的控件因为“隧道传递”的机制会最后拿到通知,但是会获得最优的处理资格,这里因为事件处理做了组件化封装,所以不妨用嵌套实现举个例子:
<TouchableHighlight onPress={this._onPressButtonH}>
<TouchableOpacity onPress={this._onPressButton}>
<View style={styles.topView}>
<Image source={require(‘./imgs/topBanner.png‘)} style={styles.topBanner}/>
<Image source={require(‘./imgs/xiaoren.png‘)} style={styles.xiaoren}/>
</View>
</TouchableOpacity>
</TouchableHighlight>
使用TouchableHighlight对原来的处理再进行一次包裹,这里嵌套层次更深的是TouchableOpacity的处理,所以其优先级更高,并默认截断了冒泡过程,所以结果是点击态为透明,回调函数为_onPressButton。
6、样式
RN并没有完整的实现CSS,而是使用JS来给应用添加,官方文档给出了详细的原因(http://facebook.github.io/react-native/docs/style.html#content),讲述了从CSS的缺陷到CSS Extension无法解决的问题,最终到CSS in JS(inline styles)的提出。语法上不同之处见下图:
1、 非数字变量值需要使用单引号;
2、 数字值不用添加px/pt单位,React会自动补全(pt);
3、 属性最后添加逗号;
4、 使用驼峰法书写。
使用方法:
这样的定义和使用方法有如下好处(翻译自官方幻灯片):
1、 并非使用时直接书写css来实现内嵌,而是在代码别处统一按照一定规则书写,使用时引用即可;
2、 “style”是比“class”更好的名字,我们是想去对元素进行样式修改,而非对类型修改;
3、 这里的修改并没有直接修改DOM,而是对React Virtual DOM进行了修改,而且是以修改元素的方式的进行的。
为了解决给元素赋特定样式的问题(例如考虑不同状态的样式变化),RN可以使用自定义函数进行样式覆盖:
示例中先定义React的一个PropTypes属性isDepressed记录是否该button被点击,使用m函数时参数传递根据该属性进行判断是否需要添加depressed style。flex的介绍推荐阮一峰的博客。
7、通信机制
这方面介绍的文章很多,推荐三篇文章如下:
A、JS和OC的通信
摘两段话介绍JS和OC的通信机制:
“普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。React Native也是以此为基础,通过各种手段,实现了在OC定义一个模块方法,JS可以直接调用这个模块方法并还可以无缝衔接回调。”
“JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行”
B、JS和Java的通信
传统的JS 调用 Java方法(Jsbridge、onprompt、log 及 addjavascriptinterface等)RN 中并没有采用,而是借助 MessageQueue 及模块配置表,将调用转化为{moduleID, methodID,callbackID,args},处理端在模块配置表里查找注册的模块与方法并调用。具体推荐下面两篇文章。
http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=663
在调用条件上,OC/JAVA在调用JS是可以通过JS暴露的接口,在使用模块配置表转化后直接调用的,而JS调用OC/JAVA则是通过事件触发。
8、跨平台组件
RN崇尚的是:“一次学习,随处可写”,而非“一次编码,随处可运行”。这两者的细微差别在于,RN的app 应该对各个平台做适配,而非让他们变得一样。这样的适配不仅需要平台的支持,也需要开发人员的工作。因为语言使用JS进行了统一,所以在实现多平台开发时,业务逻辑复用是比较简单的,重点在于UI的适配,RN给出的原则是:“尽可能地复用代码”。
在Facebook给的F8-APP教程(makeitopen.com,中文翻译:http://f8-app.liaohuqiu.net)中,RN给出了跨平台实现的建议:关键是使用平台抽象,确认app可复用组件,并进行分化。以F8为例:
A、多样化更小的组件
iOS和Android顶栏TAB风格有明显不同,iOS是圆角,Android一般为下划线,然而其功能和样式往往是差不多的,所以在此可以使用同一个组件来实现,使用Platform module判定平台,并在样式上根据平台进行区分:
在render时不需要严格考虑平台多样性,使用同样的代码进行实现,而在styles.button的定义中再进行区分。
如果涉及到的样式不一致情况更多,可以考虑使用Platform module提供的方法获得当前平台,使用条件语句对组件使用不同的style。
B、对不同平台实现进行分离
在设计差异比较大的情况下,一般采用分离的方法,如iOS和Android的导航栏:iOS采用底部导航,Android使用侧滑菜单,其动画、样式、内容都有很大差异。第一种情况中可能有不同的style的定义,但是都在同一组件中,在这种情况下可以采用定义“同名不同平台”组件的方法。如创建两个不同的组件,其中一个叫 FBTabsView.ios.js,另外一个叫 FBTabsView.android.js。React Native 会根据运行的平台自动找到并加载他们。在分离组件的情况下,平台特定性就可以得到利用,比如在FBTabsView.android.js中我们可以使用Android平台特有的DrawerLayoutAndroid组件,在FBTabsView.ios.js中可以使用iOS平台特有的TabBarIOS组件。这样可以在跨平台的同时保持原有平台的特征。
- WHY—RN的优劣
这个问题是我一直在考虑的,一个新产品/概念的提出一定伴随大量的质疑和比较,RN其实是在开发效率和用户体验之间做的一个权衡。在上一章节介绍RN基本概念和特性时,已经能体现很多其优势劣势,这个问题也有很多人对此进行过探讨,这里简单总结下。
1、RN的优势
A、接近Native的用户体验
RN项目成员Tom Occhino发表的React Native: Bringing modern web techniques to mobile详细描述了RN的设计理念。Occhino认为尽管Native开发成本更高,但现阶段Native仍然是必须的,因为Web的用户体验仍无法超越Native: Native的原生控件有更好的体验;Native有更好的手势识别;Native有更合适的线程模型,尽管Web Worker可以解决一部分问题,但如图像解码、文本渲染仍无法多线程渲染,这影响了Web的流畅性。Occhino没提到的还有Native能实现更丰富细腻的动画效果,归根结底是现阶段Native具有更好的人机交互体验。
在用户体验上RN无疑是超越Web很多的存在,因为使用的Native控件,甚至在体验上可以接近Native。
B、较低的开发成本
学习一种方式,可以在不同平台上实现APP,可以大量节省人力成本,同时提高开发效率。甚至在维护上也是非常方便和高效的。在小公司开发人员开销是一笔不小的开支,RN的出现会对此有所改观。
C、拥有不错的调试工具
虽然在开发原生APP,但是RN提供了基于浏览器的调试环境,允许在浏览器中进行变量监控、界面元素查看、渲染时间比较等,简单截图几张如下:
Perf monitor
Start Systrace
使用Brew安装trace2html,可以使用systrace功能:brew install trace2html
右侧工具可以选择进行更方便的查看,比如左右拖动进行缩放;拖动显示选择区域的持续事件;双击显示详情:
D、接近Native的性能
天猫的鬼道在内存、CPU和启动时间对RN、Native、Web进行了比对,发布在Qcon全球软件开发大会上,得出的结论是在总体性能上 以上是关于React Native那些事的主要内容,如果未能解决你的问题,请参考以下文章 React Native开源库react-native-image-crop-picker使用图解 使用 Relay 和 React-Native 时的条件片段或嵌入的根容器 “ES7 React/Redux/GraphQL/React-Native 片段”不适用于 javascript 文件。除了安装它,我还需要配置其他东西吗?