React Native 初体验

前言

转眼又年尾了,JS 都忘的差不多了 :( 。

本文是 iOS 程序员的 JavaScript 之旅 系列的第四篇,这个系列主要用于记录我在学习 JavaScript 中的一点心得,文中内容由于参考文献版本不一定是最新 & 个人理解水平有限,可能出现一些错误,还请谅解。如果有任何讨论 / 建议 / 意见,欢迎评论留言或是邮件联系。

为什么会想到 RN ?

提到 JavaScript,你或许会想前端 JS、后端 JS,再抛出 iOS 几个字,你或许会想到 JavaScriptCore,如果再说到热更新,JSPatch 又会从脑海里出来( :) R.I.P),如果我再补充一条跨平台,那么 React Native 便会脱口而出。React Native 已经火了有一段时间了,它不是第一个想要一统全平台的解决方案,虽然本文执笔时,RN 的正式版本还只是 0.49,距离正式版还有好些距离,但不得不说,它的完善度和使用量都在日渐提高,国内也有不少大公司的应用中已经集成了 RN,我想,尽管在后续的版本更新中,RN 的一些语法和组件实现都还会有变动,但 RN 已经是一套较为成熟和可用的解决方案了,另外,Debug 阶段不用反复的 build,发布时也不受苹果审核的控制,这项不算新颖但又正在崛起的一门技术,为什么不学习一下呢?

初体验?想多了

本文并不是 RN 的手把手起步教学,也不会介绍 RN 的前世今生和发展历程,事实上我也还在学习之中。如果你是一个 RN 小白,那么我推荐你阅读这篇 RayWenderlich 的入门教学 了解 RN 的基础用法、环境配置等等,然后,可以看看油管上的 React Native Practical Flex Layout,看看如何站在 RN 的角度,去分析、实现一个真实的业务场景。本文就是在看完该视频之后写下的,主要用于记录一些心得和感受。

我做了什么呢

当我跟完 RW 上的教程之后,我是非常迷茫的,文章我看懂了,Demo Project 也跟着做了,可是我总觉得我没法动手去实现一个真实的业务场景,怎么办?拿起你的手机,打开你心爱的 App,挑一个内容相对丰富的界面,撸起袖子干!

我相中的是 手机YY 的个人中心页面:

有导航栏、有 CollectionView、有小红点、有动画(日常任务那里是个轮播控件),看起来是个五脏俱全界面了。根据实际的界面效果,运用上面油管视频中的布局策略,我们可以大概搭一个界面框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 整个 “Collection View”
render() {
// 计算初始化 item 数量,得出 initialSize
return (
<ListView contentContainerStyle={styles.list}
initialListSize={initialSize}
dataSource={this.state.dataSource}
renderRow={this._renderItem.bind(this)}
/>
);
}

// ...

// 每个 “cell” 的视图
_renderItem(itemData, sectionID, itemID) {
var itemNum = parseInt(itemID);
if (itemNum == 0) {
return (
<View style={styles.userCell}>
<UserInfoCell />
</View>
);
}
if (itemNum == 1) {
return (
<View style={styles.dailyMissionCell}>
<DailyMissionCell />
</View>
);
}
return (
<TouchableHighlight onPress={() => self._pressItem(itemID)} underlayColor='rgba(0,0,0,0)' >
<View style={styles.infoCell}>
<InfoCell />
</View>
</TouchableHighlight >
);
}

(貌似 Hexo 的高亮插件对 RN 的支持不太好)

界面的具体实现同样不在本文的讨论范围之内,如有需要,可以直接下载 Github 源码,最终,实现如下效果(没做 Tabbar):

_(色差好大)_

遇到的问题

本文着重记录和讨论我在实现该工程时遇到的一些问题以及我自己的思考。

构建视图大不同

在以往使用 Native 构建视图界面的时候,我们通常是在 viewDidLoad 或是 awakeFromNib 中(也就是运行时),利用一系列的 addSubviewinsertSubview 来建立视图之间的关系,而整个的布局系统,更侧重的也是视图之间的关系建立,尤其是同级视图之间的关系,我们通常会用 Autolayout 指定他们的对齐、大小、位置等等布局关系。

但 RN 中引入的概念,是一个名为 JSX 的东西,它写起来像极了 HTML,通过标签来声明视图并建立视图关系,它的布局系统,是一套名为 Flexbox 的体系。视图的父子关系通过<标签></标签>作用域来决定,而同级视图之间的关系,通常是由其容器(父视图)的布局参数来决定的。其中最最常用,通用性也最好的自然是流式布局(Flex Layout),使用了该种布局方式的容器内的所有视图,都会以横或是纵的方向流式铺开,这也直接导致了一个问题,就是当视图布局不是简单的流式布局时,我们需要加入多层容器,来实现目标布局。

*图中的红框是纵向布局容器,蓝框是横向布局容器*

另外,一些视图并不会始终保持初始化时的状态,而是后续随着用户操作而显示/消失/改变样式,在 Native 开发中,我们通常以 addSubview,removeFromSuperview 或是 view.someProperty = someValue 的方式来达到目的,但在 RN 中则是一套完全不同的思想,首先我们没有 add 或 remove 这样的概念,视图的构建是使用 JSX 描述性的实现的,但 JSX 语法中支持执行逻辑,依然为动态加载视图提供了可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
render (){
var dynamicView = getViewWhenNeeded();

return (
<View>
{dynamicView} //当然也可以直接写成 {getViewWhenNeeded()}
</View>
);
}
getViewWhenNeeded(){
if (!this.state.needed){
return null;
}
return (
<View style={styles.yourStyle}>
</View>
);
}
switchViewState(){
this.setState({
needed : !this.state.needed
})
}

render() 是 RN 中的 Component 视图刷新的入口,也是我们构建该视图和其子视图的地方,当我们需要改变视图状态时,需要调用 component.setState() ,来更新该视图的状态,然后在 render() 方法中,根据不同的状态值,来显示不同的视图。
render() 的返回值我们可以看出,我们是返回了一个完整的视图树,这就意味着,当一个视图状态更新时,其所有的子视图也都重新被构造了一次,直观上就感觉这样的做法效率偏低,似乎有“牵一发而动全身”的感觉,所以将视图拆分开来,做成单独的 Component 子类,或许会是一个好的习惯,而且经过我不严谨的实验,似乎将子视图缓存起来,RN 是不会重新创建视图的(这里后续还要更严谨的验证,目前只是猜想):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
render(){
if (!this.cachedView){
this.cachedView = (
<View style={styles.yourStyle}>
</View>
);
}

return (
<View>
{this.cachedView}
</View>
);
}

Component.animate()?

在 Native 开发中,我们通常会使用 UIView.animate() 方法,或是 UIViewPropertyAnimator 创建实例进行视图动画,事实上,利用这些系统方法(以及 Core Graphics 层的动画方法)并配合上一些简单的参数,我们可以做出许多 酷炫的界面动画。而 RN 中,情况则略有不同,从上面更新视图的逻辑我们可以看到,RN 的视图更像是由一个状态机来维护的,当状态变化时(setState()),底层调用 render(),从而视图根据状态量重新绘制。动画机制也是类似,可动画视图在创建时,就需要绑定对应的状态量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_createViewsIfNeeded() {
if (!this.animationView) {
this.animatedViewAngle = new Animated.Value(0);
const angle = this.animatedViewAngle.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})
var transformedRotation = { transform: [{ rotate: angle }] };
this.animationView = (
<Animated.View style={[styles.animationView, transformedRotation]}>
</Animated.View>
);
}
}

动画的执行,实际上是对改状态量的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_startAnimations() {
this._createViewsIfNeeded();
if (this.timerId) {
return;
}
var self = this;
this.timerId = setInterval(
() => {
self.animatedViewAngle.setValue(0);
Animated.timing(
self.animatedViewAngle,
{
toValue: 1,
duration: 2 * 1000
}
).start();
self._startAnimations();
}, 3 * 1000);
}

虽然看起来并不复杂,而且 interpolate() 方法也可以很好的把实际的动画参数范围抽象成 0 ~ 100%,但在动画较复杂,状态量较多的时候,感觉这样的方式还是不如 UIView.animate() 的 block 形式来的直观简单。

不再被需要的视图变量

正如前文所说的,RN 的视图依靠状态量驱动,而如果你在网上搜索示例代码,也会看到许多 render() 方法都直接返回一个完整的 <View></View> 结构。因此,如果不考虑前文所提到的可能存在的效率问题,如下的存储视图的逻辑在大部分场景下都不需要了:

1
2
3
4
5
this.cachedView = <View></View>;

render(){
//use this.cachedView
}

而且,似乎 RN 的视图并不支持创建后的修改,不知道是我没找到正确的姿势还是 RN 设计就是希望你只在 render() 之中修改视图状态,考虑 Swift 中的如下代码:

1
2
3
func somethingHappened(){
self.textLabel.text = "Hey ,something has gone wrong";
}

而在 RN 中,我们可能无法直接修改视图的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
somethingHappened(){
// this.cachedTextLabel.text = "Hey ,something has gone wrong"; 并没有这种写法
this.setState({
tips:"Hey ,something has gone wrong"
});
}

render(){
return (
<Text>
{this.state.tips}
</Text>
);
}

总结感想

在之前的文章中,我曾经写到过:“JavaScript 并不是一门面向对象的语言,不要尝试用面向对象的思想去解读它的特性”,虽然如今为了迎合 OOP 的大潮,ES6 已经推出了一系列 OOP 概念中才存在的关键字,但我认为相同的道理也适用于 RN 和纯 Native,RN 不能算是 Native 的延伸,而是一套新的机制,我们不能用传统构建 iOS 应用的思维去理解、使用它,在实现这个 RN 个人主页的过程中,我感到的最大的不适应就是其各项机制与 Native 的不同,我不再能直接对目标视图进行操作修改,而需要通过 setState() 手动将某些视图设置为 dirty,触发刷新;动画不再是在特定的 block 中修改视图属性,而是预先定义好绑定关系,对状态量进行改变。我想,后续的开发学习中,应该还存在着不少不适应的地方,试着去理解 RN 的设计思维,系统机制,思考为什么它和 Native 存在种种差异,我想这样的对比总结出来的结论,也将对后续各种项目的开发起到非常好的帮助作用。

本文的内容及代码都是我在学习、实践中总结出来的,包括实现的 Demo 工程,必然存在不足的地方,仅供参考。如果有任何的意见/建议,欢迎邮件联系。