**React Native开发技术的几个关键:**
1、本地持久化;
2、网络请求;
3、离线缓存;
4、主题更换;
5、组织项目结构;
6、适配IOS和Android平台;
7、功能调试;
8、组件化的思想;
9、组件的属性与状态;
10、组件的生命周期;
11、使用组件搭建界面—>a:搭建cell组件;b:搭建静态表格页;c:搭建app基本骨架
12、组件间通信
** 组件化的思想**
React Native是React在移动端的跨平台方案。如果想更快地理解和掌握React Native开发,就必须先了解React。
React是FaceBook开源的一个前端框架,它起源于 Facebook 的内部项目,并于 2013 年 5 月开源。因为React 拥有较高的性能,代码逻辑非常简单,所以越来越多的人已开始关注和使用它,目前该框架在Github上已经有7万+star。
React采用组件化的方式开发,通过将view构建成组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。有一句话说的很形象:在React中,构建应用就像搭积木一样。
因此,如果想掌握React Native,就必须先了解React中的组件。
那么问题来了,什么是组件呢?
在React中,在UI上每一个功能相对独立的模块就会被定义为组件。 相对小的组件可以通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建。
因此,整个UI是一个通过小组件构成的大组件,而且每个组件只关心自己部分的逻辑,彼此独立。
React认为一个组件应该具有如下特征:
**可组合**(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件;
**可重用**(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景;
**可维护**(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护;
**组件的属性与状态**
在React Native(React.js)里,组件所持有的数据分为两种:
属性(props):组件的props是不可变的,它只能从其他的组件(例如父组件)传递过来。状态(state):组件的state是可变的,它负责处理与用户的交互。在通过用户点击事件等操作以后,如果使得当前组件的某个
state发生了改变,那么当前组件就会触发render()方法刷新自己。
**组件的生命周期**
和iOS开发里ViewController的生命周期类似,组件也有生命周期,大致分为三大阶段:
Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM
**DOM是前端的一个概念,暂时可以粗略理解为一个页面的树形结构。**
在每个阶段都有相应的状态和与之对应的回调函数,具体可以看下图:
重要的回调函数:
render()
该函数是组件的渲染回调函数,该函数是必须实现的,并且必须返回一个组件或一个包含多个子组件的组件。
注意:该函数可以被调用多次:初始化时的渲染以及state改变以后的渲染都会调用这个函数。
componentDidMount()
在初始化渲染执行之后立刻调用一次,也就是说,在这个函数调用时,当前组件已经渲染完毕了,相当于iOS开发中ViewController里的viewDidLoad方法。
我们通常在这个方法里执行网络请求操作。
componentWillReceiveProps(object nextProps)
在当前组件接收到新的 props 的时候调用。此函数可以作为 react 在 prop 传入之后, render() 渲染之前更新 state 的机会。新的props可以从参数里取到,老的 props 可以通过 this.props 获取到。
注意:在初始化渲染的时候,该方法不会调用。
shouldComponentUpdate(object nextProps, object nextState):
在接收到新的 props 或者 state,将要渲染之前调用。如果确定新的 props 和 state 不会导致组件更新,则此处应该 返回 false,这样组件就不会更新,减少了性能上不必要的损耗。
注意:该方法在初始化渲染的时候不会调用。
componentWillUnmount()
在组件从 DOM 中移除的时候立刻被调用。例如当前页面点击返回键跳转到上一页面的时候就会调用。
搭建app基本骨架:TabBar + NavigationBar
做移动开发的朋友们应该比较了解,底部TabBar,顶部NavigationBar是移动app很主流的一个全局界面方案。然而在原生的React Native组件里面,没有将二者整合在一起的组件。幸运的是,有一个第三方组件比较好的将二者整合到了一起:react-native-tab-navigator.
在它的主页告诉我们其导入方式是在项目主目录下执行:npm install react-native-tab-navigator —save命令。但是我建议使用yarn来引入所有第三方的组件:yarn add react-native-tab-navigator。因为使用npm命令安装第三方组件的时候有时会出现问题。而且建议引入第三方组件的时候都是用yarn来操作,比较保险一点。
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 40 41 42 |
|
在确认react-native-tab-navigator组件下载到了npm文件夹以后,就可以在项目中导入使用了
这里定义的是HomePage组件,是这个Demo用来管理这些tab的组件。
因为这个Demo一共有四个tab,所以将渲染的tab的代码抽取出来作为单独的一个函数:_renderTab。该函数有四个参数:
-
Component:当前tab被点击后显示的组件。
-
selectedTab:当前tab的唯一标识。
-
title:当前tab的标题。
-
renderIcon:当前tab的图标。
在_renderTab方法里,我们返回一个TabNavigator.Item组件,除了一些关于tab的props的定义以外,我们将属于该tab的组件填充了进去:
1 |
|
组件间通信
既然React项目是以组件为单位搭建的,那么一定少不了组件之间的数据和事件的传递,也就是组件之间的通信。
组件间通信分为两大类:
-
有直接关系或间接关系的组件之间通信
-
无直接关系或间接关系的组件之间通信
有直接关系或间接关系的组件之间通信
我个人是这么理解父组件和子组件的关系的:
如果A组件包含了B组件,或者说在A组件里创建了B组件,那么A组件就是B组件的父组件;反过来B组件就是A组件的子组件,是有直接关系的组件。
比如:
一个界面的导航栏组件是整个页面组件的子组件,因为这个导航栏组件被包含在了当前的页面组件当中。
从这个页面跳转到的下一个页面是当前页面的子组件:因为被包含在了当前页面组件的Navigator里。
再加上子组件和子组件的通信,直接或间接关系组件之间的通信就分为下面这三种情况:
父组件向子组件传递数据和事件。
子组件向父组件传递消息和事件。
子组件向子组件传递消息和事件。
父组件向子组件传递数据和事件:通过对子组件的属性赋值来实现。
在上面我们看到,在给页面布局的时候我们使用了导航栏组件:
1 |
|
在这里,当前页面组件将'我的'对象,以及this.state.theme.styles.navBar对象分别赋值给了导航栏组件。而导航栏接收到这两个值以后,在其内部可以通过this.props.title和this.props.style来获取到这两个值。这样一来,就实现了父组件向子组件传递数据的功能。
子组件向父组件传递消息、数据:通过父组件给子组件一个闭包(回调函数)来实现
举一个点击最热标签页面的一个cell进行回调后实现界面跳转的例子:
既然这个cell组件是在最热标签页面组件中生成的,那么cell组件就是其子组件:
1 2 3 4 5 |
|
这个renderRow()函数是ListView组件用来渲染每一行Cell的函数,必须返回一个Cell组件才可以。在这里我们自定义了一个RespositoryCell组件作为其Cell组件。
我们可以看到,这里面有5个props被赋值了,其中,onSelect和onFavorite被赋予了函数:
onSelect回调的是点击cell之后在最热标签页面里跳转页面的函数onSelectRepository()。
onFavorite则回调的是更改最热标签页面对应收藏按钮状态的函数onFavorite(未被收藏时是空心的星;被收藏的话是实心的星)。
下面在RespositoryCell组件内部看一下这两个函数是如何回调的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
由上一节我们知道,父组件给子组件的props传值后,子组件里面对应的props就被赋值了。在这RespositoryCell组件里面就是this.props.onSelect和this.props.onFavorite。这两个函数被赋给了两个TouchableOpacity组件的onPress里面。这里的()=>可以理解为为传递事件,表示当该控件被点击后的事件。
不同的是,this.props.onFavorite()是可以将两个值回传给其父组件。细心的同学会发现,在给RespositoryCell传值的时候,是有两个返回值存在的。
注意,在这里的TouchableOpacity和上文提到的TouchableHighlight类似,都可以让非可点击组件变成可点击组件。区别在于配合TouchableOpacity使用时,点击后无高亮效果。而TouchableHighlight默认是有高亮效果的。
OK,现在我们知道了父组件和子组件是如何传递数据和事件了:
父组件到子组件:通过直接给属性赋值
子组件到父组件:通过父组件给子组件传递回调函数
需要注意的是,上面讲的都是直接关系的父子组件,其实还有间接关系的组件,也就是两个组件之间有一个或多个组件连接着,比如父组件的子组件的子组件。这些组件之间的通信都可以通过上述的方法来实现,只不过是中间跨过多少层的区别而已。
需要注意的是,这里说的父组件和子组件的通信,不仅仅包括这种直接关系,还包括间接关系,而间接关系的组件就是该组件与其子组件的子组件的关系。
所以无论中间隔了多少组件,只要是存在于这种关系链上的组件,都可以用上述两种方式来传递数据和事件。
兄弟组件之间的通信
虽然不是包含于被包含,由谁创建了谁的关系,但是同一父组件下的几个子组件(兄弟组件)也算得上是有间接关系了(中间夹着共同的父组件)。
那么在同一父组件下的两个子组件是如何传递数据呢?
答案是通过二者所共享的父组件的state来传递数据的
因为我们知道触发组件的渲染是通过setState方法的。因此,如果两个子组件都使用了他们的父组件的同一个state来渲染自己。
那么当其中一个子组件触发了setState,更新了这个共享的父组件的state,继而触发了父组件的render()方法,那么这两个子组件都会依据这个更新后的state来刷新自己,这样一来,就实现了子组件的数据传递。
到现在就讲完了有直接或间接关系的组件之间的通信,下面来讲一下无直接关系或间接关系的组件之间的通信:
无直接关系和间接关系的组件之间通信
如果两个组件从属于不同的关系链既没有直接关系,也没有间接关系(例如不同模块下的两个页面组件),那么想实现通信的话,就需要通过通知机制,或者本地持久化方案来实现。在这里先介绍一下通知机制,而本地持久化会在下面单拿出一节来专门讲解。
通知机制可以通过这个Demo的收藏功能来讲解:
先大致介绍一下收藏的需求:
在最热标签页或者语言趋势页面如果点击了收藏按钮,那么在收藏页面就会增加被收藏的项目(注意,点击收藏按钮后不进行网络请求,也就是说,收藏页面是没有网络请求的)。
而如果在收藏页面中取消了收藏,就需要在最热标签页面或语言趋势页面中对应的项目里面更新取消收藏的效果(同样没有网络请求)。
因为这三个页面从属于不同模块, 而且又不是以网络请求的方式刷新列表,所以如果要满足上述需求,就需要使用通知或者本地存储的方式来实现。
在这个Demo中,第一个需求采用的是本地持久化方案,第二个需求采用的是通知机制。本地持久化方案我会在下一节单独介绍,在本节先讲一下在React Native里如何使用通知机制:
在React Native里面有专门的组件专门负责通知这一功能,它的名字是:DeviceEventEmitter,它是React Native内置的组件,我们可以直接将它导入到工程里。导入的方式和其他内置的组件一样:
1 2 3 4 5 6 7 8 9 |
|
既然是通知,那么自然有接收的一方,也有发送的一方,这两个组件都需要引入该通知组件。
在接收的一方需要注册某个通知:
比如在该Demo里面,如果在收藏页面修改了收藏的状态,就要给最热标签页面发送一个通知。所以首先就需要在最热标签页面注册一个通知,注册通知后才能确保将来可以收到某个频道上的通知
componentDidMount() {
...
this.listener = DeviceEventEmitter.addListener('favoriteChanged_popular',()=> {
this.isFavoriteChanged = true;
})
}
在这里通过给DeviceEventEmitter的addListener方法传入两个参数来进行通知的注册:
第一个参数是通知的频道,用来区别其他的通知。
第二个参数是需要调用的函数:在这里只是将this.isFavoriteChanged赋值为YES。它的目的是在于将来如果该值等于YES,就进行界面的再渲染,更新收藏状态。
1 2 3 4 |
|
在这里,拦截了收藏按钮的点击。还记得么?这里onFavorite()函数就是上面说的点击收藏按钮的回调。
我们在这里发送了通知,只需传入频道名称即可。
是不是很easy?
OK,到这里我们讲完了组件间的通信这一块,简单回想一下各种关系的组件之间的通信方案。
下面我们来讲一下在React Native里的本地持久化的方案。
本地持久化
需要注意的是,有注册,就要有注销,在组件被卸载之前,需要将监听解除:
1 2 3 4 5 |
|
这样,我们搞定了通知的注册,就可以在程序的任意地方发送通知了。在该需求中,我们需要拦截住在收藏页面里对项目的收藏按钮的点击,只要点击了,就发送通知:告知最热标签页面收藏的状态改变了:
AsyncStorage常用接口
根据键来获取值,获取的结果会放在回调函数中:
1 |
|
根据键来设置值:
1 |
|
根据键来移除项:
1 |
|
获取所有的键:
1 |
|
设置多项,其中 keyValuePairs 是字符串的二维数组,比如:[['k1', 'val1'], ['k2', 'val2']]:
1 |
|
获取多项,其中 keys 是字符串数组,比如:['k1', 'k2']:
1 |
|
删除多项,其中 keys 是字符串数组,比如:['k1', 'k2']:
1 |
|
清除所有的项目:
static clear(callback:(error))
AsyncStorage使用注意事项
需要注意的是,在使用AsyncStorage的时候,setItem里面传入的数组或字典等对象需要使用JSON.stringtify()方法把他们解析成JSON字符串:
1 |
|
这里,favoriteKeys是一个数组。
反过来,在getItem方法里获取数组或字典等对象的时候需要使用JSON.parse方法将他们解析成对象:
1 2 3 4 5 6 7 8 9 |
|
这里,result被解析出来后是一个数组。
网络请求
在React Native中,经常使用Fetch函数来实现网络请求,它支持GET和POST请求并返回一个Promise对象,这个对象包含一个正确的结果和一个错误的结果。
来看一下用Fetch发起的POST请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
从上面的代码中,我们可以大致看到:Fetch函数中,第一个参数是请求url,第二个参数是一个字典,包括方法,请求头,请求体等信息。
随后的then和catch分别捕捉了fetch函数的返回值:一个Promise对象的正确结果和错误结果。注意,这里面有两个then,其中第二个then把第一个then的结果拿了过来。而第一个then做的事情是把网络请求的结果转化为JSON对象。
那么什么是Promise对象呢?
Promise 是异步编程的一种解决方案,Promise对象可以获取某个异步操作的消息。它里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
它分为三种状态:
Pending(进行中)、Resolved(已成功)和Rejected(已失败)
它的构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject:
resolve函数的作用:将Promise对象的状态从“未完成”变成“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;。
reject函数的作用:将Promise对象的状态从“未完成”变成“成功”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
举个例子来看一下:
1 2 3 4 5 6 7 8 |
|
这里resolve和reject的结果会分别被配套使用的Fetch函数的.then和.catch捕捉。
我个人的理解是:如果某个异步操作的返回值是一个Promise对象,那么我们就可以分别使用.then和.catch来捕捉正确和错误的结果。
再看一下GET请求:
1 2 3 4 5 6 7 8 |
|
因为只是GET请求,所以不需要配置请求体,而且因为这个fetch函数返回值是一个Promise对象, 所以我们可以用.then和.catch来捕捉正确和错误的结果。
在项目中,我们可以创建一个抓们负责网络请求的工具HttpUtils类,封装GET和POST请求。看一下一个简单的封装:
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 |
|
离线缓存
离线缓存技术可以利用上文提到的Fetch和AsyncStorage实现,将请求url作为key,将返回的结果作为值存入本地数据里。
在下一次请求之前查询是否有缓存,缓存是否过期,如果有缓存并且没有过期,则拿到缓存之后,立即返回进行处理。否则继续进行网络请求。
而且即使没有网络,最终返回错误,也可以拿到缓存数据,立即返回。
来看一下在该项目里面是如何实现离线缓存的:
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 |
|
在上面的方法中,包含了获取本地缓存和网络请求的两个方法。
首先是尝试获取本地缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
在这里,AsyncStorage.getItem方法的结果也可以使用Promise对象来包装。因此,this.fetchLocalRespository(url)的结果也就可以被.then和.catch捕捉到了。
如果获取本地缓存失败,就会调用网络请求:
1 2 3 4 5 6 7 8 9 10 11 |
|
主题更换
这个Demo有一个主题更换的需求,在主题设置页点击某个颜色之后,全app的颜色方案就会改变:
我们只需要将四个模块的第一个页面的主题修改即可,因为第二个页面的主题都是从第一个页面传进去的,所以只要第一个页面的主题改变了即可。
但是,我们应该不能在选择新主题之后同时向这四个页面都发送通知,命令它们修改自己的页面,而是应该采取一个更加优雅的方法来解决这个问题:使用父类。
新建一个BaseCommon.js页面,作为这四个页面的父类。在这个父类里面接收主题更改的通知,并更新自己的主题。这样一来,继承它的这四个页面就都会刷新自己:
来看一下这个父类的定义:
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 |
|
在更新主题页面的更新主题事件:
1 2 3 4 5 6 7 |
|
功能调试
我们可以使用浏览器的开发者工具来调试React Native项目,可以通过打断点的方式来看数据信息以及方法的调用:
-
首先在iOS模拟器中点击command + D,然后再弹出菜单里点击Debug JS Remotely。随后就打开了浏览器进入了调试。
浏览器一般会展示下面的页面,然后点击command + option + J进入真生的调试界面。
点击最上方的Sources,然后点击左侧debuggerWorker.js下的localhost:8081,就可以看到目录文件。点击需要调试的文件,在行数栏就可以打断点了。
适配iOS和Android平台
因为React Native讲求的是一份代码跑在两个平台上,而客观上这两个平台又有一些不一样的地方,所以就需要在别要的时候做一下两个平台的适配。
例如导航栏:在iOS设备中是存在导航栏的,而安卓设备上是没有的。所以在定制导航栏的时候,在不同平台下给导航栏设置不同的高度:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
建议在调试程序的时候,同时打开iOS和Android的模拟器进行调试,因为有些地方可能在某个平台上是没问题的,但是另一个平台上有问题,这就需要使用Platform来区分平台。
组织项目结构
在终端输入react-native demo --version 0.44.0命令以后,就会初始化一个React Native版本为0.44.0的项目。这个最初项目里面直接就包含了iOS和Android的工程文件夹,可以用对应的IDE打开后编译运行。
在新建一个React Native项目之后的根目录结构是这样的:
或者也可以根目录下输入react-native run-ios或者react-native run-android指令, 就会自动打开模拟器运行项目(前提是安装了相应的开发环境)。
但是一个比较完整的项目仅仅有这些类别的文件是不够的,还需要一些工具类,模型类,资源等文件。为了很好地区分它们,使项目结构一目了然,需要组织好项目文件夹以及类的命名,下面是我将教程里的文件夹命名和结构稍加修改后的一个方案,可供大家参考:
总结
从最开始的FlexBox布局的学习到现在这个项目的总结完成有了快两个月的时间了。我在这里说一下这段学习过程中的一些感受:
关于学习成本
我觉得这一点应该是所有未接触到React Native的人最关心的一点了,所以我将它放到了总结里的第一位。我在这里取两种典型的群体来做比较:
-
只会某种Native开发但是不会JavaScript等前端知识的人群。
-
只会前端知识但是不会任何一种Native开发的人群。
对于这两种人群来说,在React Native的学习过程中成本都不小。但不同的是,这两种人群的学习成本在整个学习过程中的不同阶段是不一样的。怎么说呢?
对于第一种人群,因为缺乏前端相关知识,所以在组建的布局,以及JavaScript的语法上会有点吃力。而这两点恰恰是React Native学习的敲门砖,因此,对于这种群体,在学习React Native的初期会比较吃力,学习成本很大。
关于如何配合视频来学习
在结合视频学习的时候一定要跟上思路,如果讲师是边写代码边讲解,就一定要弄清楚每一行代码的意义在哪里,为什么要这么写,千万不要怕浪费时间而快速略过。停下脚步来思考实际上是节省时间:因为如果你不试着去理解代码和讲师的思路,在后来你会越来越看不懂,反而浪费大量时间重新回头看。
所以我认为最好是先听一遍讲师讲的内容,理清思路,然后再动手写代码,这样效率会比较高,在将来出现的问题也会更少。
学习参考资料
下面是我近1个半月以来收集的比较好的React Native入门资料和博客,分享给大家: