React Native 调研报告

前端之家收集整理的这篇文章主要介绍了React Native 调研报告前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

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。这套框架最大的亮点有俩:

  1. 使用Javascript编写应用逻辑,保持APP的原生性,而不像HTML5那样对UI做出妥协,交互上有着优质的用户体验;
  2. 基于状态驱动的界面更新机制,而不是传统意义上通过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代码中,如果一个类对象想直调用另一个类对象的简单情况,那么主调用模块必须要知道的是:

  1. 调用模块的地址在哪儿?
  2. 调用模块的方法名是什么?
  3. 需要传入哪些参数进去?

    也即一个完整的可执行的调用地址必须有三个单元组成:(模块地址、方法名、参数),这三者缺一不可,这些信息的获得主要通过头文件机制和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注册它自己,并暴露出自己哪些属性想被暴露出去,哪些方法可以被调用。诀窍就在于:

  1. 在声明你的类的时候声明自己遵循React Native提供的RCTBridgeModule协议(RCT是ReaCT的简写);
  2. 在实现的文件添加RCT_EXPORT_MODULE();
  3. 对于你想暴露的方法使用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" };
}

在JS模块中可以直接获取函数返回的参数:

console.log(CalendarManager.firstDayOfTheWeek);

3.4.2 OC本地模块调用JS模块

OB本地的类对象要想调用JS模块里面的方法,也必须首先遵循3.4.1中提到的RCTBridgeModel协议,编译器创建的模块配置表除了有上述OC的模块remoteModules外,还保存了JS模块localModulesRCTBridgeModel 协议中提供了一个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,以发送和接收消息的方式实现调用,其原理图如下:

具体实现代码如下:

首先,在OC本地代码中发送通知EventReminder

#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协议中的RCTBridgesendDeviceEventWithName对象来实现,除了直接调用方法的方式,还可以采用通知的方式间接调用(从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 技术,保持技术跟进即可。

猜你在找的React相关文章