Facebook三月份开源了React Native iOS平台的框架,让移动开发人员和web开发者都各自兴奋了一把:native的移动开发者想的比较多的估计是Facebook的那句:“learn once,write everywhere”,而web开发者兴奋的估计是,不需要学习iOS那陌生的OC或者swift语言,用自己熟悉的javascript语言就可以开发原生的移动APP了。那么新推出的react native 能否承载的了两大阵营的开发者的期待了。本人及同事对react native做了一段时间的调研,心中渐渐有了自己的答案:
本文假定读者熟悉iOS APP开发,但对web前端开发的知识匮乏(如本人一样),在此基础上试图讲清楚react native 到底是什么;怎么用;以及当前是否值得使用。
1.React Native 是什么?
首先,react native 到底是个什么东东:它是Facebook开源的一套框架,其目的在于使用JavaScript语言编写iOS native的控件。更直白的讲就是,你用JS(JavaScript,下同)写的代码通过react-native lib桥接到Xcode中写的标准iOS程序中。在JS程序中,开发者可以使用react native定义的一套和cocoa touch中UI控件类等价的类,来完成UI层的开发工作,Xcode 编译器 会利用 react-native lib将JS写的代码编译成iOS原生的UI组件,下图展示了利用Xcode的视图调试功能展示了JS代码编译的结果,可以看到,这些JS语言最终确实是被编译成了UIView等对象,而不是H5界面经常使用的webview,有了这个认识之后,我们对react native就不在那么陌生了。
综上,你可以认为react native是一套能够让你使用Javascript而不是传统的Objective-C或者Swift,编写iOS APP 界面逻辑(MVC框架中的V层)的框架。使用过swift得同学甚至还会觉得JS和swift的语法上又不少雷同的地方(好吧,swift五仁月饼果然名不虚传),比如定义一个变量都是用关键字var。这套框架最大的亮点有俩:
- 使用Javascript编写应用逻辑,保持APP的原生性,而不像HTML5那样对UI做出妥协,交互上有着优质的用户体验;
基于状态驱动的界面更新机制,而不是传统意义上通过MVC的Controller来集中控制界面的更新工作,将UI和自定义的某一个或者某几个状态变量函数绑定,当这些变量发生变化时,这些状态变量便会驱动相应的UI模块重绘;
2.React Native 的开发环境
使用React Native 首先要搭建环境,使用React Native 进行iOS 开发,其环境如下图所示:
可以直接按照Facebook 官网搭建文档的指导来搭建React Native 开发环境,基本上没有坑,能够很迅速的完成。
除了运行环境的创建,还需要编写 JavaScript 代码的环境,Xcode 并非是最好的工具!Javascript 好用的编辑器网上很多,比如Sublime Text、atom等。
3. React Native 技术构成
3.1基本构成元素
React Native 的库同时包含了OC代码和javascript 代码,由这两种代码共同提供了一套用于构建界面UI系统的元素,包括但不限于:
- OC传统UI组件;
- OC上的手势识别及事件响应系统(如TouchableHighlight);
基于流的布局系统;
翻阅Facebook官网上的API文档,可以发现它基本上是实现了Cocoa Touch 框架上的最常用的UI控件:
基本上我们用JS写代码也是使用这些基本组件来构建我们的UI界面的。除此之外,你也可以在OC中,定制自己的模块,通过桥接的方式来在React Native 中使用。
3.2 React Native 中的事件响应系统
本地APP 和web 端的最大区别就是本地APP有着完美的事件响应系统,用户能够获得更好的用户体验。在React Native中也提供了一套事件响应系统,扒一扒React Native 的源码(在 ResponderEventPlugin.js 文件中),能够窥到React native事件响应的基本流程:
可见React native 的响应系统和Cocoa touch类似,一个view 如果想要对事件作出响应,它只需要实现函数:
- View.props.onStartShouldSetResponder: (evt) => true,- 当前view是否想作为touch 的响应者?
- View.props.onMoveShouldSetResponder: (evt) => true,- 当前view是否想作为move 事件 的响应者?
如果返回true,尝试要变成第一响应者,那么下面两个函数中的一个会被调用:
- View.props.onResponderGrant: (evt) => {} -当前view是第一响应者,在这里展示响应的交互效果(如背景色变化等)和事件触发的其他逻辑;
View.props.onResponderReject: (evt) => {} - 其他view是第一响应者;
考虑到响应系统的复杂性,React Native 在对事件响应封装的基础上实现了一些抽象类,如类似Cocoa Touch 中UIButton 的 TouchbleHighlight,你可以向使用view一样将它放到你希望有交互效果的地方。我们通过下图来看看如何使用TouchbleHighlight:
3.3 React native UI更新逻辑
在Cocoa Touch 系统中,UI更新是典型的MVC模式:Controller 通过数据的变更,来更新view层的展示,但在React Native 中却大相径庭:React Native 通过状态机的机制来驱动整个view层的更新。在开始介绍这一块之前不得不得先说一下React Native 的渲染方式:
从之前图片中给出的代码片段中读者也能窥出这种构建页面的方法和HTML语言很像:通过标签系统构建出分层的页面逻辑(父子关系),布局代码则采用CSS 的方式通过单独的代码来控制,这样显示的将业务逻辑和布局逻辑分开,使得整个代码层次逻辑更清晰。
在React Native中,整个UI都是一个component树:前面我们提到的ListView等UI组建都是一种具体的component,React Native通过将component树编译成一个virtual-DOM:虚拟文档对象模型,熟悉HTML的读者可能对于这个DOM非常熟悉,没错,就是那个DOM,整个的UI的关系可以通过这个virtual-DOM很清晰的体现,而且更重要的是,React Native的UI更新逻辑也是依赖于这个树来实现的:我们知道,笼统的讲,一个页面的更新,肯定是由数据的变更来驱动的,比如网络数据的更新或者是用户触摸导致的touch事件的发生(以及后续业务逻辑的跟进),那么如何将这些数据的变更和界面的刷新相绑定呢?如何知道哪一块的数据变更后需要刷新哪一块的UI呢?要知道每次数据更新都重绘整个界面实在是一个吃力不讨好的事情:不仅你的APP处于一种高负载运行状态,而且用户体验也不好。React Native很巧妙的通过用户提供的state变量维着一个状态机,通过将这个状态机来驱动virtual DOM树的UI更新,如下图所示:
设计好了state信息之后,React Native会根据代码逻辑计算出那一块的DOM组件需要进行更新,整个过程不需要开发者来主动的干预,开发者只需要建立好state系统,并根据数据变化来维护state信息即可,React Native会在后台为你做好这一切。
那么一个component 中的state 是什么呢?其实就是一个属性,比如一个bool值或者数组或者任何其他JS支持的类型。一个component对象其实是包含了两种类型的属性的:property和state,前者主要是一些固定值的属性,后者则是那些数据会发生变化,并且这种变化会导致界面某一部分重绘的属性,比如列表页里面的数据源等,如何区分一个属性是应该被归类为property还是state,Facebook 的 React 官网文档:think in react上有详细的介绍,这里就不再赘述。
3.4 React Native的通信机制
在讲诉具体的通信原理之前,我们首先来看一下,在代码实现上是怎样的。我们这里说的通信,很大程度上指的是我们用javascript写的模块和OC写的本地模块直接能否互相调用,如下图所示:
在具体深入细节之前,先想一下在纯OC代码中,如果一个类对象想直调用另一个类对象的简单情况,那么主调用模块必须要知道的是:
- 被调用模块的地址在哪儿?
- 被调用模块的方法名是什么?
需要传入哪些参数进去?
也即一个完整的可执行的调用地址必须有三个单元组成:(模块地址、方法名、参数),这三者缺一不可,这些信息的获得主要通过头文件机制和Cocoa Touch 运行时系统来提供。而React Native的通信原理也是如此:
我们可以将React Native里用JS代码写的模块和OC写的本地模块看作是两个互相陌生的城市,那么很显然,这两个城市之间的人要想有效的沟通,必须要彼此有一张对方城市的地图才行。那么在React Native世界里,这两张地图就是模块配置表,它看上去大概是酱紫滴(一下部分资料来源于Bang’s blog):
既然双方的通信可以通过模块配置表来解决,那么现在问题就简化为:如何向编译和运行时系统提供这张表了,为方便进一步分析,这两我们将通信氛围JS模块调用OC本地模块和OC本地模块调用JS模块两部分进行讨论:
3.4.1 JS模块调用OC本地模块
一个OC模块也即OC写的一个普通的类,在默认情况下是无法被Javascript 运行时系统捕获并进而被调用的,它必须要向编译及运行时系统提供or注册它自己,并暴露出自己哪些属性想被暴露出去,哪些方法可以被调用。诀窍就在于:
- 在声明你的类的时候声明自己遵循React Native提供的RCTBridgeModule协议(RCT是ReaCT的简写);
- 在实现的文件中添加宏
RCT_EXPORT_MODULE();
对于你想暴露的方法使用
RCT_EXPORT_METHOD()
宏进行封装;那么在被调用的OC模块里需要添加的代码如下(代码来自Facebook官网):
// CalendarManager.h
#import "RCTBridgeModule.h"
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
// CalendarManager.m
@implementation CalendarManager
RCT_EXPORT_MODULE();
......
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@",name,location);
}
@end
主调模块JS中得调用方式如下:
var CalendarManager = require('NativeModules').CalendarManager;
CalendarManager.addEvent('Birthday Party','4 Privet Drive,Surrey');
- string (NSString)
- number (NSInteger,float,double,CGFloat,NSNumber)
- boolean (BOOL,NSNumber)
- array (NSArray) of any types from this list
- map (NSDictionary) with string keys and values of any type from this list
- function (RCTResponseSenderBlock)
没错!就是这么简单! Awesome, isn’t it ? ^_^
除此之外,对于OC模块想暴露给JS模块的参数,可以通过constantsToExport
方法提供,该方法返回的是一个字典,示例代码如下:
In OC模块:
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
console.log(CalendarManager.firstDayOfTheWeek);
3.4.2 OC本地模块调用JS模块
OB本地的类对象要想调用JS模块里面的方法,也必须首先遵循3.4.1中提到的RCTBridgeModel
协议,编译器创建的模块配置表除了有上述OC的模块remoteModules
外,还保存了JS模块localModules
。RCTBridgeModel
协议中提供了一个RCTBridge
属性对象,该对象提供了访问JS模块的方法,代码如下:
/** * This method is used to call functions in the JavaScript application context. * It is primarily intended for use by modules that require two-way communication * with the JavaScript code. */
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args;
除了这种直接的调用方式之外,FacebookFacebook React Native 官网还提供了一种间接实现JS模块调用的方法,即通过RCTEventDispatcher
,以发送和接收消息的方式实现调用,其原理图如下:
具体实现代码如下:
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation CalendarManager
@synthesize bridge = _bridge;
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *eventName = notification.userInfo[@"name"];
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
body:@{@"name": eventName}];
}
@end
其次,在JS模块中监听EventReminder
通知,并添加相应的通知响应函数:
var subscription = DeviceEventEmitter.addListener(
'EventReminder',(reminder) => console.log(reminder.name)
);
...
// Don't forget to unsubscribe,typically in componentWillUnmount subscription.remove();
通过接收、发送通知的方式可以降低OC模块和JS模块的耦合度,而这种方式的实现同样是通过RCTBridge
的直接调用方式来实现的,通过查看RCTEventDispatcher
中发送通知的sendDeviceEventWithName
的源码实现即可发现:
- (void)sendDeviceEventWithName:(NSString *)name body:(id)body
{
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
args:body ? @[name,body] : @[name]];
}
综上,我们可以认为,OC模块直接调用JS模块的通信方式主要通过RCTBridgeModel
协议中的RCTBridge
的sendDeviceEventWithName
对象来实现,除了直接调用该方法的方式,还可以采用通知的方式间接调用(从FacebookFacebook React Native 官网上仅介绍了通知的方式,也许能看出这是Facebook推荐的方式,至于具体的使用,还需开发者依情境便宜行事)。
3.4.3 React Native 的通信机制总结
通过以上两节代码演示,我们能够很快的实现OC模块和JS模块的双路通信,基本上,无论是OC调JS还是JS调OC,其依赖的核心就是双侧模块提供的模块配置表(remote and local),至于其详细实现原理,其参看Bang’s blog,这里不再详述。
3.5. React Native 的UI布局机制
无论是web页面还是Native的本地页面,在开发中UI布局都是很重要的一环,能否兼容多重尺寸的页面、设备,是开发者面临的首要问题。随着苹果iPhone手机屏幕尺寸的越来越多样化(目前至少有iphone4s/5s/6/6plus四种尺寸了),苹果也越来越趋向于将UI布局重任放在了autolayout上面了,autolayout是一种典型的相对布局方法,开发者通过可视化编辑环境xib或者storyboard对UI组件添加约束,运行时系统通过自动布局引擎,根据实际的屏幕尺寸,计算出UI中每个控件的Frame信息,从而实现UI的布局。
和Cocoa Touch 不同的是,React Native 在UI布局上采用了一个完全不同的系统: HTML CSS,也就目前主流网页的流式布局方式,开发者可以将每一个的布局信息写入单独的style表中,将布局和业务逻辑分析,开发者使用HTML CSS的语法完成布局信息:
var styles = StyleSheet.create({ scrollView: { backgroundColor: '#6A85B1',height: 300,},button: { margin: 7,padding: 5,alignItems: 'center',backgroundColor: '#eaeaea',borderRadius: 3,buttonContents: { flexDirection: 'row',width: 64,height: 64,img: { width: 64,} });
除了使用标准的HTML CSS方式进行布局,React Native还支持Flexbox模块的布局方式,根据其官网说明,FlexBox Layout module旨在提供一种更高效的方式来布局,以动态的决定在一个container中的子项的对其、居中、间隔甚至是尺寸大小的方式。
FlexBox布局对象只有两类:容器(container)和容器内的子项(item),如下图所示:
对于容器和子项分别有六七个布局属性关键字,罗列如下:
应用于Container的属性:
display
flex-direction
flex-wrap
flex-flow // = flex-direction + flex-wrap
justify-content
align-items
align-content
应用于Item的属性:
order
flex-grow
flex-shrink
flex-basis
flex // = flex-grow + flex-shrink + flex-basis
align-self
CSS中使用FlexBox只需要直接添加相应的关键字即可,如下代码所示,具体每一种布局关键字的意义可以通过这篇文章获取:
.flex-container {
/* We first create a flex layout context */
display: flex;
/* Then we define the flow direction and if we allow the items to wrap
* Remember this is the same as:
* flex-direction: row;
* flex-wrap: wrap;
*/
flex-flow: row wrap;
/* Then we define how is distributed the remaining space */
justify-content: space-around;
}
HTML CSS Style的布局方式相对于iOS 的自动布局方式,其动态性更好,但只能通过纯代码的方式来写布局,着实让人有些痛苦,而且对于广大没有web前端开发经验的iOS移动端猿猿们来说,CSS的布局方式初一上手,还是觉得有些陌生:基本上你要换一种思维方式才能考虑清楚具体的布局细节,而且对于更复杂的动态的场景,这种布局方式可能更难以实现和维护。
4. 目前,使用React Native的时机是否成熟
在React Native大热的同时,我们要谨慎的探讨一下使用React Native 的时机是否成熟这个问题。调研的这一段时间,我们发现有一下几点值得注意:
4.1 JS模块和OC模块的数据交互只能通过字典(dictionary)传递
字典在OC模块中是一种比较松散的数据结构,如果考虑使用React Native负责UI界面的绘制工作,OC模块负责数据的处理,那么二者的交互载体只能是字典。OC定义的model类对象(如使用core data时创建的model 对象)无法直接传递给JS模块使用,还必须要提前转为字典才行,这无疑多了一层处理逻辑,势必会带来一些潜在的风险。
以使用CoreData存储数据为例,我们的整个数据层的交互将是这样子的:
这种只能通过字典来传值的限制,就使得我们没法直接将OC模块中的数据对象直接作为JS模块里面驱动页面更新的state属性。我们将不得不添加一个中间层来转换数据的这种变化已映射到JS模块里驱动UI层的更新。
4.2 React Native的learn once,write everywhere 的实现还有待时日
Facebook在力推React Native的时候强调它的最大特点是:Learn once,write where. 但目前的实际情况是,React Native Android 预计2015年10月才发布,这对希望三端(Web/iOS/Android)架构一致的用户而言也算个风险。而且细看React Native iOS 框架里面,还有很多和iOS本地模块紧密耦合的模块,比如以iOS结尾的若干component都是iOS才有的,使用了这些模块的代码,将来想直接在Android上运行恐怕是不可能的事情,那么React Native在这一点上离真正的跨平台还有不少路要走:
**COMPONENTS**
ActivityIndicatorIOS
DatePickerIOS
Image
ListView
MapView
Navigator
NavigatorIOS
PickerIOS
ScrollView
SliderIOS
SwitchIOS
TabBarIOS
TabBarIOS.Item
4.3 React Native 中Listview 性能问题
在github的React Native有一个issue格外让人担忧: ListView renders all rows? 其中有几个评论揭示了这样一个事实:React Native的ListView可能一次性的渲染了所有的rows(cells):
@ide I'm a noob at instruments profiler... So here's brief summary from me taking a look at it.
cpu profile looks like most of the time is spent here (recursing through subviews),in RCTView.m :
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
In memory profile major causes of persisted memory (645 MB total) are:
VM: CG Graphics Data (410 MB)
VM: CoreAnimation (141 MB)
VM: JS Garbage Collector (61 MB)
...
Unmount the ListView component,and total persisted memory drops to 74 MB total:
VM: JS Garbage Collector 58 MB
VM: CoreUI image data
...
Kureev commented 24 days ago
It's totally insane: my iPhone 5c crashes after 700 list items. If I'm going to write a chat - it's blocking for me.
Also I got a lot of "Cannot find single active touch"
samfriend commented 14 days ago
my < ListView pagingEnabled={true} onEndReached={this.loadAnotherFiftyArticles} >
50 rows/pages of < Image / > < Title/ > < Description/ >
3rd Load append (total 150)
Received memory warning
Received memory warning
Received memory warning
Crash Physical iPhone 6 Plus
Also I got a lot of "Cannot find single active touch" time to time
为了证明网友们的担忧,这里我们用Xcode的view透视工具看了一下React Native 官网提供的Demo: UIExplorer的ListView,看看到底是否是一次性重绘了所有的rows,下图展示了这个Demo运行时的界面:
然后我们使用Xcode 的 Debug View Hierarchy来查看这个试图的界面层级结构,结果让我们触目惊心:它果然对所有的row进行了绘制!
再看一个scrollView的情况,好吧,看完我整个人都不好了:
如果情况真是这样,那就这一条就足有让我们有理由选择放弃使用React Native了,至少暂时是!
4.4 React Native 的UI布局系统不尽如人意
我们知道,React Native采用的web前端的HTML CSS 也即流式布局。整个UI界面都是通过树形结构构建起来,布局也是基于此,而且需要全手动打造。使用过纯代码的方式写iOS 上的autolayout 布局的童鞋相比一定被这种非可视化的布局方式深深刺痛吧:你必须要在脑海里将所有的UI展示效果转换成为纷繁复杂的布局约束。
autolayout可视化的布局是React Native所最欠缺的
采用CSS布局的另一个劣势是,相比较于传统的Native 布局方式,精确性控制的不是很好,最终布局效果可能和设计师的初衷相差甚远。在自动布局autolayout 中我们可以通过sizeclass针对横竖屏做定制化的布局工作,但是使用CSS目前来看还没法实现这种方式。
4.4 总结
通过上诉的分析,我们发现React Native在性能、开发便利性等方面还存在很大的不足。目前来看,它还没实现它所倡导的“Learn Once, Write Everywhere”的目标,但带来的问题却不少,别的不说,单就ListView的性能问题就是一个最大的瓶颈。React Native还处在一个初期摸索阶段,它的下一阶段发展如何,还要看它的老东家Facebook接下来的动作。因此,笔者建议目前已有的开发项目中不要冒险采用React Native 技术,保持技术跟进即可。