在进行了2个星期的基础学习(Flexbox, React.js, JSX, JavaScript)以后,想经过一个实战项目来提升React Native的开发水平,因而找到了下面这个项目:javascript
这是我在学习贾鹏辉老师在慕课网上的一个很火的React Native实战的教程后,写出的课程Demo。该课程是慕课网里很火的一个React Native课程,当初在看了课程介绍和课程安排以为讲解的点仍是很全的,因此绝不犹豫地买了下来。css
从看视频,敲代码到重构,改bug,大概花了2个多星期的时间,除了调用友盟的SDK以及CodePush集成以外,其余的部分都基本完成了,JavaScript代码占据了95%,基本上算是一个纯React Native项目,并且同时能够在iOS和Android设备上运行: html
并且比较吸引人的是该项目能够实现多个主题的切换: 前端
主题切换的技术实现会在下文给出。java
用一个动图来过一遍大体的需求: react
Demo GitHub地址:GitHubPopular-SJ 能够按照README文件里的方法运行该项目。android
上传到GitHub已通过贾老师容许ios
值得一提的是:这确实是一门物有所值的课程,可让想入门React Native的开发者少走不少弯路。虽然我上传的Demo能够实现视频里大部分功能,可是通过调试,修改后的代码信息量仍是颇有限的,并且老师在视频中讲解的不少关于实际开发的知识点在代码中并无体现出来,因此仍是建议各位报名参加课程来提升本身的开发水平。css3
首先用一张思惟导图来看一下第二节讲的内容: git
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认为一个组件应该具备以下特征:
举个🌰,咱们看一下这个Demo使用的导航栏:
封装好的导航栏就能够被称之为一个组件,它符合上述三个特色:
在了解了组件的基本概念之后,咱们来看一下组件其余的一些相关知识。
在React Native(React.js)里,组件所持有的数据分为两种:
render()
方法刷新本身。举一个这个项目的收藏页面来讲:
咱们能够看到这个页面有两个子页面,一个是‘最热’页面(组件),另外一个是‘趋势‘页面(组件)。那么这两个组件都有什么props和state呢?
首先看一下props: 因为props是从其父组件传递过来的,那么可想而知,props的声明应该是在当前组件的父组件里来作。在React Native中,一般props的声明是和当前组件的声明放在一块儿的:
//最热子页面
<FavoriteTabPage {...this.props} tabLabel='最热' flag={FlAG_STORAGE.flag_popular}/>
//趋势子页面
<FavoriteTabPage {...this.props} tabLabel='趋势' flag={FlAG_STORAGE.flag_trending}/>
复制代码
在这里,收藏页面是父组件,而最热页面和趋势页面是其子组件。在收藏页面组件里声明了最热页面和趋势页面的组件。
并且咱们也能够看到,最热页面和趋势页面组件都用的是同一个组件:FavoriteTabPage
,而这两个页面的不一样点只在于传入的两个props的不一样:tabLabel
和flag
。
而在FavoriteTabPage
组件内部,若是想调用flag这个props,可使用this.props.flag
来调用。
再来看一下state:
下面是最热和趋势页面的组件:
class FavoriteTabPage extends Component{
//组件的构造方法
constructor(props){
super(props);
this.state={
dataSource:new ListView.DataSource({rowHasChanged:(r1,r2)=>r1!==r2}),
isLoading:false,
}
}
...
}
复制代码
这里面定义了两个state:
这两个state都是未来可能常常变化的。好比在网络请求之后,列表的数据源会被替换掉,这个时候就要调用
this.setState({
//把新的值newDataArr对象传给dataSource
dataSource:newDataArr
})
复制代码
来触发render()
方法来刷新列表组件。
和iOS开发里ViewController
的生命周期相似,组件也有生命周期,大体分为三大阶段:
DOM是前端的一个概念,暂时能够粗略理解为一个页面的树形结构。
在每一个阶段都有相应的状态和与之对应的回调函数,具体能够看下图:
从上图中咱们能够看到,React 为每一个状态都提供了两种回调函数,will 函数在进入状态以前调用,did 函数在进入状态以后调用。
在这里讲一下这其中几个重要的回调函数:
该函数是组件的渲染回调函数,该函数是必须实现的,而且必须返回一个组件或一个包含多个子组件的组件。
注意:该函数能够被调用屡次:初始化时的渲染以及state改变之后的渲染都会调用这个函数。
在初始化渲染执行以后马上调用一次,也就是说,在这个函数调用时,当前组件已经渲染完毕了,至关于iOS开发中ViewController
里的viewDidLoad
方法。
咱们一般在这个方法里执行网络请求操做。
在当前组件接收到新的 props 的时候调用。此函数能够做为 react 在 prop 传入以后, render() 渲染以前更新 state 的机会。新的props能够从参数里取到,老的 props 能够经过 this.props 获取到。
注意:在初始化渲染的时候,该方法不会调用。
在接收到新的 props 或者 state,将要渲染以前调用。若是肯定新的 props 和 state 不会致使组件更新,则此处应该 返回 false,这样组件就不会更新,减小了性能上没必要要的损耗。
注意:该方法在初始化渲染的时候不会调用。
在组件从 DOM 中移除的时候马上被调用。例如当前页面点击返回键跳转到上一页面的时候就会调用。
咱们一般在这个方法里移除通知。具体作法在后文会提到。
到此,已经讲解了一些组件相关的知识,下面来看一下咱们如何使用组件来搭建界面。
在这里咱们举几个例子来看一下在React Native里搭建View的方式。
首先咱们来看一下最热页面的cell是如何布局的:
首先举一个在最热标签页面列表里的一个cell为例,讲解一下一个简单的UI组件是如何实现的:
咱们把该组件定名为:RespositoryCell
,结合代码来看一下具体的实现:
export default class RespositoryCell extends Component{
...
render(){
//获取当前cell的数据赋值给item
let item = this.props.projectModel.item?this.props.projectModel.item:this.props.projectModel;
//收藏按钮
let favoriteButton = <TouchableOpacity
onPress={()=>this.onPressFavorite()}
>
<Image
style={[styles.favoriteImageStyle,this.props.theme.styles.tabBarSelectedIcon]}
source={this.state.favoriteIcon}
/>
</TouchableOpacity>
return(
<TouchableOpacity
onPress={this.props.onSelect}
style={styles.container}
>
//整个cell的view
<View style={styles.cellContainerViewStyle}>
//1. 项目名称
<Text style={styles.repositoryTitleStyle}>{item.full_name}</Text>
//2. 项目介绍
<Text style={styles.repositoryDescriptionStyle}>{item.description}</Text>
//3. 底部 container
<View style={styles.bottomContainerViewStyle}>
//3.1 做者container
<View style={styles.authorContainerViewStyle}>
//3.11 做者名称
<Text style={styles.bottomTextStyle}>Author:</Text>
//3.12 做者头像
<Image
style={styles.authorAvatarImageStyle}
source={{uri:item.owner.avatar_url}}
/>
</View>
//3.2 star container
<View style={styles.starContainerViewStyle}>
//3.21 star标题
<Text style={styles.bottomTextStyle}>Starts:</Text>
//3.21 star数量
<Text style={styles.bottomTextStyle}>{item.stargazers_count}</Text>
</View>
//3.3 收藏按钮
{favoriteButton}
</View>
</View>
</TouchableOpacity>)
}
}
复制代码
这里省略了处理交互事件等的函数,为了让你们集中在cell的布局和样式上。
RespositoryCell
组件,它继承于Component
,也就是组件类,便是说,声明组件的时候必须都要继承与这个类。View
组件包裹着,里面第一层有三个子组件:两个Text
组件和一个做为底部背景的View
组件。 - 底部背景的View
组件又有三个子组件:View
组件(显示做者信息),View
组件(显示star信息),收藏按钮。试着结合代码来看一下下面的图片,能够看出组件的实际布局与代码的布局是高度一致的:
然而仅仅定义组件的层级关系是不够的,咱们还须要定义组件的样式(例如图片组件的大小样式等等),这时候就经过定义一个样式的对象(一般使用常量对象)来定义一些须要使用的样式:
//样式常量
const styles =StyleSheet.create({
//项目cell的背景view的style
cellContainerViewStyle:{
//背景色
backgroundColor:'white',
//内边距
padding:10,
//外边距
marginTop:4,
marginLeft:6,
marginRight:6,
marginVertical:2,
//边框
borderWidth:0.3,
borderColor:'#dddddd',
borderRadius:1,
//iOS的阴影
shadowColor:'#b5b5b5',
shadowOffset:{width:3,height:2},
shadowOpacity:0.4,
shadowRadius:1,
//Android的阴影
elevation:2
},
//项目标题的style
repositoryTitleStyle:{
fontSize:15,
marginBottom:2,
color:'#212121',
},
//项目介绍的style
repositoryDescriptionStyle:{
fontSize:12,
marginBottom:2,
color:'#757575'
},
//底部container的style
bottomContainerViewStyle:{
flexDirection:'row',
justifyContent:'space-between'
},
//做者container的style
authorContainerViewStyle:{
flexDirection:'row',
alignItems:'center'
},
//做者头像图片的style
authorAvatarImageStyle:{
width:16,
height:16
},
//星星container的style
starContainerViewStyle: {
flexDirection:'row',
alignItems:'center'
},
//底部文字的style
bottomTextStyle:{
fontSize:11,
},
//收藏按钮的图片的style
favoriteImageStyle:{
width:18,
height:18
}
})
复制代码
在上面这段代码里定义了RespositoryCell
组件所使用的全部样式,经过将其赋值给对应子组件的style属性来实现对组件样式的修改,例如咱们看一下项目标题的组件和其样式的定义:
<Text style={styles.repositoryTitleStyle}>{item.full_name}</Text>
复制代码
在这里,咱们首先定义了一个Text组件用来显示项目的标题。而后将styles.repositoryTitleStyle
赋给了当前Text组件的style,而标题的具体内容,则经过item.full_name
来获取。
须要注意的是,在JSX的语法中,对象须要被{}来包裹住,不然会被认为是常量。好比,若是这里写成:
<Text style={styles.repositoryTitleStyle}>item.full_name</Text>
复制代码
那么全部项目cell的标题则都会显示为''item.full_name'',有图有真相:
这是初学者比较常犯的错误,因此要注意:在搭建页面的时候,必定要区分是对象仍是常量。若是是对象就必需要用大括号括起来!若是是对象就必需要用大括号括起来!若是是对象就必需要用大括号括起来!
这里每一个样式里面的长,宽,内外边距,以及
flexDirection
等flexBox相关的布局属性就不介绍了。能够经过查找本文最后的相关连接来学习。
在React Native中搭建我的页,设置页这种静态表格页面的时候,能够用ScrollView
组件包裹各类封装好的cell组件的形式实现。看一下这个Demo的我的页的效果图和代码实现:
咱们在项目中新建一个JavaScript文件,取名为取名为MinePage.js
。该文件就是我的页面的实现。结合代码来看一下它的实现(删除了处理点击cell的逻辑处理代码):
//区域一:引用区:
//引用React,Component(组件类)以及React Native中自带的组件
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image,
ScrollView,
TouchableHighlight,
} from 'react-native';
//引入项目中定义的其余组件(页面组件)和常量,路径为相对路径
import NavigationBar from '../../common/NavigationBar'
import {MORE_MENU} from '../../common/MoreMenu'
import GlobalStyles from '../../../res/styles/GlobalStyles'
import ViewUtil from '../../util/ViewUtils'
import {FLAG_LANGUAGE}from '../../dao/LanguageDao'
import AboutPage from './AboutPage'
import CustomKeyPage from './CustomKeyPage'
import SortPage from './SortKeyPage'
import AboutMePage from './AboutMePage'
import CustomThemePage from './CustomThemePage'
import BaseComponent from '../../base/BaseCommon'
//区域二:页面组件定义区域:
export default class MinePage extends BaseComponent {
...
//渲染页面中List中每一个cell的统一函数
createSettingItem(tag,icon,text){
return ViewUtil.createSettingItem(()=>this.onClick(tag),icon,text,this.state.theme.styles.tabBarSelectedIcon,null);
}
render(){
return <View style={GlobalStyles.listViewContainerStyle}>
<NavigationBar
title={'个人'}
style={this.state.theme.styles.navBar}
/>
<ScrollView>
{/*=============项目信息Section=============*/}
<TouchableHighlight
underlayColor= 'transparent'
onPress={()=>this.onClick(MORE_MENU.About)}
>
<View style={styles.itemInfoItemStyle}>
<View style={{flexDirection:'row',alignItems:'center'}}>
<Image source={require('../../../res/images/ic_trending.png')}
style={[{width:40,height:40,marginRight:10},this.state.theme.styles.tabBarSelectedIcon]}
/>
<Text>GitHub Popular 项目信息</Text>
</View>
<Image source={require('../../../res/images/ic_tiaozhuan.png')}
style={[{height:22,width:22},this.state.theme.styles.tabBarSelectedIcon]}
/>
</View>
</TouchableHighlight>
{/*分割线*/}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*=============趋势管理Section=============*/}
<Text style={styles.groupTitleStyle}>趋势管理</Text>
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*自定义语言*/}
{this.createSettingItem(MORE_MENU.Custom_Language,require('../../../res/images/ic_custom_language.png'),'自定义语言')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*语言排序*/}
{this.createSettingItem(MORE_MENU.Sort_Language,require('../../../res/images/ic_swap_vert.png'),'语言排序')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*=============标签管理Section=============*/}
<Text style={styles.groupTitleStyle}>标签管理</Text>
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*自定义标签*/}
{this.createSettingItem(MORE_MENU.Custom_Key,require('../../../res/images/ic_custom_language.png'),'自定义标签')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*标签排序*/}
{this.createSettingItem(MORE_MENU.Sort_Key,require('../../../res/images/ic_swap_vert.png'),'标签排序')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*标签移除*/}
{this.createSettingItem(MORE_MENU.Remove_Key,require('../../../res/images/ic_remove.png'),'标签移除')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*=============设置Section=============*/}
<Text style={styles.groupTitleStyle}>设置</Text>
{/*自定义主题*/}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{this.createSettingItem(MORE_MENU.Custom_Theme,require('../../../res/images/ic_view_quilt.png'),'自定义主题')}
<View style={GlobalStyles.cellBottomLineStyle}></View>
{/*展现自定义主题页面*/}
{this.renderCustomTheme()}
</ScrollView>
</View>
}
}
//区域三:定义页面组件样式区:
const styles = StyleSheet.create({
itemInfoItemStyle:{
flexDirection:'row',
justifyContent:'space-between',
alignItems:'center',
padding:10,
height:76,
backgroundColor:'white'
},
groupTitleStyle:{
marginLeft:10,
marginTop:15,
marginBottom:6,
color:'gray'
}
});
复制代码
在上面的代码中,咱们能够看到一个页面组件的全貌,它大体分为三个区域:
下面两个区域在上一节已经介绍过。第一个区域,引用区域通常写在组件文件的开头,在这里通常是须要引入该组件须要的其余组件或者常量。
如今看一下该组件的render()
函数,它返回了用来包裹整个页面的View
组件,该组件有两个子组件
createSettingItem(tag,icon,text){
return ViewUtil.createSettingItem(()=>this.onClick(tag),icon,text,this.state.theme.styles.tabBarSelectedIcon,null);
}
复制代码
能够看到这个函数传入的参数有三个:用来做标记的tag,图片 和标题文字。它的返回值经过调用ViewUtil组件的createSettingItem
方法来实现。这个方法用于统一辈子成相似布局的cell。
看一下这个函数的实现:
//ViewUtils.js
static createSettingItem(callBack,icon,text,tintColor,expandableIcon){
//若是不传入icon,则不显示
let image = null;
if (icon){
image = <Image
source={icon}
resizeMode='stretch'
style={[{width:18,height:18,marginRight:10},tintColor]}
/>
}
return (
<View style={{backgroundColor:'white'}}>
<TouchableHighlight
onPress={callBack}
underlayColor= 'transparent'
>
<View style={styles.settingItemContainerStyle}>
<View style={{flexDirection:'row',alignItems:'center'}}>
{image}
<Text>{text}</Text>
</View>
<Image source={expandableIcon?expandableIcon:require('../../res/images/ic_tiaozhuan.png')}
style={[{marginRight:0,height:22,width:22},tintColor]}//要用括号
/>
</View>
</TouchableHighlight>
</View>
)
}
复制代码
这个函数有5个参数:
由于在React Native中没有特定的Button
组件,因此实现组件的点击都是经过被TouchableHighlight
等可点击组件包裹来实现的。
经常使用的能够实现点击效果的是View
组件和Text
组件。
注意一下TouchableHighlight
里面传入的两个props:
underlayColor
设为transparent
。onPress
属性。因此,若是该cell被点击了,就会触发传入的callback。这个callback就等于当初传过来的箭头函数:ViewUtil.createSettingItem(()=>this.onClick(tag),icon,text,this.state.theme.styles.tabBarSelectedIcon,null);
复制代码
该函数是在我的页被调用的,用来实现点击cell时的跳转等操做。
注意,在这个ViewUtils类中,咱们能够定义不少经常使用的View组件,例如这种设置页面的cell,导航栏上的返回按钮等等。
如今cell的实现讲完了,下面讲一下分割线和session的title。
先来看一下分割线:
<View style={GlobalStyles.cellBottomLineStyle}></View>
复制代码
它的样式调用了GlobalStyles
的cellBottomLineStyle
。由于GlobalStyles
是全局的样式文件(单独写在了一个js文件中),可使用它来专门管理一些经常使用的样式。这样一来,咱们就不须要在不一样页面的组件页面里面重复声明样式常量了。
咱们看一下如何定义全局的样式文件:
//GlobalStyles.js
module.exports ={
//cell分割线样式
cellBottomLineStyle: {
height: 0.4,
opacity:0.5,
backgroundColor: 'darkgray',
},
//cell背景色样式
cell_container: {
flex: 1,
backgroundColor: 'white',
padding: 10,
marginLeft: 5,
marginRight: 5,
marginVertical: 3,
borderColor: '#dddddd',
borderStyle: null,
borderWidth: 0.5,
borderRadius: 2,
shadowColor: 'gray',
shadowOffset: {width:0.5, height: 0.5},
shadowOpacity: 0.4,
shadowRadius: 1,
elevation:2
},
//当前屏幕高度
window_height:height,
//当前屏幕宽度
window_width:width,
};
复制代码
由于使用了module.exports
方法,在这里定义的全局样式能够在外部随意使用。
最后,Section Title的View就比较简单了,就是一个带有灰色文字的View
组件。
<Text style={styles.groupTitleStyle}>趋势管理</Text>
复制代码
作移动开发的朋友们应该比较了解,底部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
来操做,比较保险一点。
在确认react-native-tab-navigator
组件下载到了npm文件夹之后,就能够在项目中导入使用了。下面来看一下使用方法:
//导入 react-native-tab-navigator 组件,取名为 TabNavigator(随意取名)
import TabNavigator from 'react-native-tab-navigator';
//每一个tab对应的惟一标识,能够在外部获取
export const FLAG_TAB = {
flag_popularTab: 'flag_popularTab',
flag_trendingTab: 'flag_trendingTab',
flag_favoriteTab: 'flag_favoriteTab',
flag_myTab: 'flag_myTab'
}
export default class HomePage extends BaseComponent {
constructor(props){
super(props);
let selectedTab = this.props.selectedTab?this.props.selectedTab:FLAG_TAB.flag_popularTab
this.state = {
selectedTab:selectedTab,
theme:this.props.theme
}
}
_renderTab(Component, selectedTab, title, renderIcon) {
return (
<TabNavigator.Item
selected={this.state.selectedTab === selectedTab}
title={title}
selectedTitleStyle={this.state.theme.styles.selectedTitleStyle}
renderIcon={() => <Image style={styles.tabItemImageStyle}
source={renderIcon}/>}
renderSelectedIcon={() => <Image
style={[styles.tabItemImageStyle,this.state.theme.styles.tabBarSelectedIcon]}
source={renderIcon}/>}
onPress={() => this.onSelected(selectedTab)}>
<Component {...this.props} theme={this.state.theme} homeComponent={this}/>
</TabNavigator.Item>
)
}
render() {
return (
<View style={styles.container}>
<TabNavigator
tabBarStyle={{opacity: 0.9,}}
sceneStyle={{paddingBottom: 0}}
>
{this._renderTab(PopularPage, FLAG_TAB.flag_popularTab, '最热', require('../../../res/images/ic_polular.png'))}
{this._renderTab(TrendingPage, FLAG_TAB.flag_trendingTab, '趋势', require('../../../res/images/ic_trending.png'))}
{this._renderTab(FavoritePage, FLAG_TAB.flag_favoriteTab, '收藏', require('../../../res/images/ic_favorite.png'))}
{this._renderTab(MinePage, FLAG_TAB.flag_myTab, '个人', require('../../../res/images/ic_my.png'))}
</TabNavigator>
</View>
)
}
}
复制代码
在这里我省略了其余的代码,只保留了关于搭建TabBar && NavigationBar
的代码。
这里定义的是HomePage
组件,是这个Demo用来管理这些tab的组件。
由于这个Demo一共有四个tab,因此将渲染的tab的代码抽取出来做为单独的一个函数:_renderTab
。该函数有四个参数:
在_renderTab
方法里,咱们返回一个TabNavigator.Item
组件,除了一些关于tab的props的定义之外,咱们将属于该tab的组件填充了进去:
<Component {...this.props} theme={this.state.theme} homeComponent={this}/>
复制代码
在这里,{...this.props}是将当前HomePage
的全部props赋给这个Component
。还有另外两个props也定义了进去:theme
和homeComponent
。
这里用一个常量定义了四个tab的惟一标识,须要注意的是,这个常量是能够被其余组件得到的,觉得它被export
字段修饰了。
另外,还须要注意一下HomePage
有一个属性是selectedTab
,它用来标记当前选择的tab是哪个。在constructor
方法里作了一个判断,若是没有从外部组件传进来selectedTab
,则须要初始化为FLAG_TAB.flag_popularTab
。
既然React项目是以组件为单位搭建的,那么必定少不了组件之间的数据和事件的传递,也就是组件之间的通讯。
组件间通讯分为两大类:
有直接关系或间接关系的组件之间通讯
无直接关系或间接关系的组件之间通讯
我我的是这么理解父组件和子组件的关系的:
若是A组件包含了B组件,或者说在A组件里建立了B组件,那么A组件就是B组件的父组件;反过来B组件就是A组件的子组件,是有直接关系的组件。
好比:
一个界面的导航栏组件是整个页面组件的子组件,由于这个导航栏组件被包含在了当前的页面组件当中。
从这个页面跳转到的下一个页面是当前页面的子组件:由于被包含在了当前页面组件的Navigator
里。
再加上子组件和子组件的通讯,直接或间接关系组件之间的通讯就分为下面这三种状况:
父组件向子组件传递数据和事件。
子组件向父组件传递消息和事件。
子组件向子组件传递消息和事件。
在上面咱们看到,在给页面布局的时候咱们使用了导航栏组件:
<NavigationBar
title={'个人'}
style={this.state.theme.styles.navBar}
/>
复制代码
在这里,当前页面组件将'个人'
对象,以及this.state.theme.styles.navBar
对象分别赋值给了导航栏组件。而导航栏接收到这两个值之后,在其内部能够经过this.props.title
和this.props.style
来获取到这两个值。这样一来,就实现了父组件向子组件传递数据的功能。
举一个点击最热标签页面的一个cell进行回调后实现界面跳转的例子:
既然这个cell组件是在最热标签页面组件中生成的,那么cell组件就是其子组件:
//ListView组件生成每一个cell的函数
renderRow(projectModel){
return <RespositoryCell key = {projectModel.item.id} theme={this.state.theme} projectModel={projectModel} onSelect = {()=>this.onSelectRepository(projectModel)} onFavorite={(item,isFavorite)=>this.onFavorite(item,isFavorite)}/> } 复制代码
这个renderRow()
函数是ListView
组件用来渲染每一行Cell的函数,必须返回一个Cell组件才能够。在这里咱们自定义了一个RespositoryCell
组件做为其Cell组件。
咱们能够看到,这里面有5个props被赋值了,其中,onSelect
和onFavorite
被赋予了函数:
onSelect
回调的是点击cell以后在最热标签页面里跳转页面的函数onSelectRepository()
。onFavorite
则回调的是更改最热标签页面对应收藏按钮状态的函数onFavorite
(未被收藏时是空心的星;被收藏的话是实心的星)。下面在RespositoryCell
组件内部看一下这两个函数是如何回调的:
render(){
let item = this.props.projectModel.item?this.props.projectModel.item:this.props.projectModel;
let favoriteButton = <TouchableOpacity {/*调用点击收藏的回调函数*/} onPress={()=>this.onPressFavorite()} > <Image style={[styles.favoriteImageStyle,this.props.theme.styles.tabBarSelectedIcon]} source={this.state.favoriteIcon} /> </TouchableOpacity> return( <TouchableOpacity {/*点击cell的回调函数*/} onPress={this.props.onSelect} style={styles.container} > <View style={styles.cellContainerViewStyle}> ... {favoriteButton} </View> </TouchableOpacity>) } onPressFavorite(){ this.setFavoriteState(!this.state.isFavorite); //点击收藏的回调函数 this.props.onFavorite(this.props.projectModel.item,!this.state.isFavorite) } 复制代码
由上一节咱们知道,父组件给子组件的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内置的组件,咱们能够直接将它导入到工程里。导入的方式和其余内置的组件同样:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image,
DeviceEventEmitter,
TouchableOpacity
} from 'react-native';
复制代码
既然是通知,那么天然有接收的一方,也有发送的一方,这两个组件都须要引入该通知组件。
在接收的一方须要注册某个通知:
好比在该Demo里面,若是在收藏页面修改了收藏的状态,就要给最热标签页面发送一个通知。因此首先就须要在最热标签页面注册一个通知,注册通知后才能确保未来能够收到某个频道上的通知
componentDidMount() {
...
this.listener = DeviceEventEmitter.addListener('favoriteChanged_popular',()=> {
this.isFavoriteChanged = true;
})
}
复制代码
在这里经过给DeviceEventEmitter
的addListener
方法传入两个参数来进行通知的注册:
this.isFavoriteChanged
赋值为YES。它的目的是在于未来若是该值等于YES,就进行界面的再渲染,更新收藏状态。须要注意的是,有注册,就要有注销,在组件被卸载以前,须要将监听解除:
componentWillUnmount() {
if(this.listener){
this.listener.remove();
}
}
复制代码
这样,咱们搞定了通知的注册,就能够在程序的任意地方发送通知了。在该需求中,咱们须要拦截住在收藏页面里对项目的收藏按钮的点击,只要点击了,就发送通知:告知最热标签页面收藏的状态改变了:
onFavorite(item,isFavorite){
...
DeviceEventEmitter.emit('favoriteChanged_popular');
}
复制代码
在这里,拦截了收藏按钮的点击。还记得么?这里onFavorite()
函数就是上面说的点击收藏按钮的回调。
咱们在这里发送了通知,只需传入频道名称便可。
是否是很easy?
OK,到这里咱们讲完了组件间的通讯这一块,简单回想一下各类关系的组件之间的通讯方案。
下面咱们来说一下在React Native里的本地持久化的方案。
相似于iOS 中的NSUserDefault
, AsyncStorage 是React Native中的 Key-Value 存储系统,能够作本地持久化。
首先看它主要的几个接口:
static getItem(key: string, callback:(error, result))
复制代码
static setItem(key: string, value: string, callback:(error))
复制代码
static removeItem(key: string, callback:(error))
复制代码
static getAllKeys(callback:(error, keys))
复制代码
static multiSet(keyValuePairs, callback:(errors))
复制代码
static multiGet(keys, callback:(errors, result))
复制代码
static multiRemove(keys, callback:(errors))
复制代码
static clear(callback:(error))
复制代码
须要注意的是,在使用AsyncStorage的时候,setItem里面传入的数组或字典等对象须要使用JSON.stringtify()
方法把他们解析成JSON字符串:
AsyncStorage.setItem(this.favoriteKey,JSON.stringify(favoriteKeys));
复制代码
这里,favoriteKeys是一个数组。
反过来,在getItem方法里获取数组或字典等对象的时候须要使用JSON.parse
方法将他们解析成对象:
AsyncStorage.getItem(this.favoriteKey,(error,result)=>{
if (!error) {
var favoriteKeys=[];
if (result) {
favoriteKeys=JSON.parse(result);
}
...
}
});
复制代码
这里,result被解析出来后是一个数组。
在React Native中,常用Fetch函数来实现网络请求,它支持GET和POST请求并返回一个Promise对象,这个对象包含一个正确的结果和一个错误的结果。
来看一下用Fetch发起的POST请求:
fetch('http://www.***.cn/v1/friendList', {
method: 'POST',
headers: { //header
'token': ''
},
body: JSON.stringify({ //参数
'start': '0',
'limit': '20',
})
})
.then((response) => response.json()) //把response转为json
.then((responseData) => { // 上面的转好的json
//using responseData
})
.catch((error)=> {
alert('返回错误');
})
复制代码
从上面的代码中,咱们能够大体看到: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),在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。
举个例子来看一下:
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
复制代码
这里resolve和reject的结果会分别被配套使用的Fetch函数的.then和.catch捕捉。
我我的的理解是:若是某个异步操做的返回值是一个Promise对象,那么咱们就能够分别使用.then
和.catch
来捕捉正确和错误的结果。
再看一下GET请求:
fetch(url)
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error)
})
复制代码
由于只是GET请求,因此不须要配置请求体,并且由于这个fetch函数返回值是一个Promise对象, 因此咱们能够用.then
和.catch
来捕捉正确和错误的结果。
在项目中,咱们能够建立一个抓们负责网络请求的工具HttpUtils类,封装GET和POST请求。看一下一个简单的封装:
export default class HttpUtls{
static get(url){
return new Promise((resolve,reject)=>{
fetch(url)
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error)
})
})
}
static post(url, data) {
return new Promise((resolve, reject)=>{
fetch(url,{
method:'POST',
header:{
'Accept':'application/json',
'Content-Type':'application/json',
},
body:JSON.stringify(data)
})
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error)
})
})
}
}
复制代码
离线缓存技术能够利用上文提到的Fetch
和AsyncStorage
实现,将请求url做为key,将返回的结果做为值存入本地数据里。
在下一次请求以前查询是否有缓存,缓存是否过时,若是有缓存而且没有过时,则拿到缓存以后,当即返回进行处理。不然继续进行网络请求。
并且即便没有网络,最终返回错误,也能够拿到缓存数据,当即返回。
来看一下在该项目里面是如何实现离线缓存的:
//获取数据
fetchRespository(url) {
return new Promise((resolve, reject) =>{
//首先获取本地缓存
this.fetchLocalRespository(url)
.then((wrapData)=> {
//本地缓存获取成功
if (wrapData) {
//缓存对象存在
resolve(wrapData,true);
} else {
//缓存对象不存在,进行网络请求
this.fetchNetRepository(url)
//网路请求成功
.then((data) => {
resolve(data);
})
//网路请求失败
.catch(e=> {
reject(e);
})
}
}).catch(e=> {
//本地缓存获取失败,进行网络请求
this.fetchNetRepository(url)
//网路请求成功
.then(result => {
resolve(result);
})
//网路请求失败
.catch(e=> {
reject(e);
})
})
})
}
复制代码
在上面的方法中,包含了获取本地缓存和网络请求的两个方法。
首先是尝试获取本地缓存:
//获取本地缓存
fetchLocalRespository(url){
return new Promise((resolve,reject)=>{
// 获取本地存储
AsyncStorage.getItem(url, (error, result)=>{
if (!error){
try {
//必须使用parse解析成对象
resolve(JSON.parse(result));
}catch (e){
//解析失败
reject(e);
}
}else {
//获取缓存失败
reject(error);
}
})
})
}
复制代码
在这里,
AsyncStorage.getItem
方法的结果也可使用Promise对象来包装。所以,this.fetchLocalRespository(url)
的结果也就能够被.then
和.catch
捕捉到了。
若是获取本地缓存失败,就会调用网络请求:
fetchNetRepository(url){
return new Promise((resolve,reject)=>{
fetch(url)
.then(response=>response.json())
.catch((error)=>{
reject(error);
}).then((responseData)=>{
resolve(responseData);
})
})
}
复制代码
这个Demo有一个主题更换的需求,在主题设置页点击某个颜色以后,全app的颜色方案就会改变:
咱们只须要将四个模块的第一个页面的主题修改便可,由于第二个页面的主题都是从第一个页面传进去的,因此只要第一个页面的主题改变了便可。
可是,咱们应该不能在选择新主题以后同时向这四个页面都发送通知,命令它们修改本身的页面,而是应该采起一个更加优雅的方法来解决这个问题:使用父类。
新建一个BaseCommon.js
页面,做为这四个页面的父类。在这个父类里面接收主题更改的通知,并更新本身的主题。这样一来,继承它的这四个页面就都会刷新本身:
来看一下这个父类的定义:
import React, { Component } from 'react';
import {
DeviceEventEmitter
} from 'react-native';
import {ACTION_HOME} from '../pages/Entry/HomePage'
export default class BaseComponent extends Component {
constructor(props){
super(props);
this.state={
theme:this.props.theme,
}
}
componentDidMount() {
this.baseListener = DeviceEventEmitter.addListener('ACTION_BASE',(action,parmas)=>this.changeThemeAction(action,parmas));
}
//卸载前移除通知
componentWillUnmount() {
if(this.baseListener){
this.baseListener.remove();
}
}
//接收通知
changeThemeAction(action,params){
if (ACTION_HOME.A_THEME === action){
this.onThemeChange(params);
}
}
//更新theme
onThemeChange(theme){
if(!theme)return;
this.setState({
theme:theme
})
}
}
复制代码
在更新主题页面的更新主题事件:
onSelectTheme(themeKey) {
this.themeDao.save(ThemeFlags[themeKey]);
this.props.onClose();
DeviceEventEmitter.emit('ACTION_BASE',ACTION_HOME.A_THEME,ThemeFactory.createTheme(
ThemeFlags[themeKey]
))
}
复制代码
咱们可使用浏览器的开发者工具来调试React Native项目,能够经过打断点的方式来看数据信息以及方法的调用:
command + D
,而后再弹出菜单里点击Debug JS Remotely
。随后就打开了浏览器进入了调试。command + option + J
进入真生的调试界面。Sources
,而后点击左侧debuggerWorker.js
下的localhost:8081
,就能够看到目录文件。点击须要调试的文件,在行数栏就能够打断点了。由于React Native讲求的是一份代码跑在两个平台上,而客观上这两个平台又有一些不同的地方,因此就须要在别要的时候作一下两个平台的适配。
例如导航栏:在iOS设备中是存在导航栏的,而安卓设备上是没有的。因此在定制导航栏的时候,在不一样平台下给导航栏设置不一样的高度:
import {
StyleSheet,
Platform,
} from 'react-native'
const NAV_BAR_HEIGHT_IOS = 44;
const NAV_BAR_HEIGHT_ANDROID = 50;
navBarStyle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: Platform.OS === 'ios' ? NAV_BAR_HEIGHT_IOS : NAV_BAR_HEIGHT_ANDROID,
},
复制代码
上面的Platform
是React Native内置的用于区分平台的库,能够在引入后直接使用。
建议在调试程序的时候,同时打开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的人最关心的一点了,因此我将它放到了总结里的第一位。我在这里取两种典型的群体来作比较:
对于这两种人群来讲,在React Native的学习过程当中成本都不小。但不一样的是,这两种人群的学习成本在整个学习过程当中的不一样阶段是不同的。怎么说呢?
对于第一种人群,由于缺少前端相关知识,因此在组建的布局,以及JavaScript的语法上会有点吃力。而这两点偏偏是React Native学习的敲门砖,所以,对于这种群体,在学习React Native的初期会比较吃力,学习成本很大。
在结合视频学习的时候必定要跟上思路,若是讲师是边写代码边讲解,就必定要弄清楚每一行代码的意义在哪里,为何要这么写,千万不要怕浪费时间而快速略过。停下脚步来思考其实是节省时间:由于若是你不试着去理解代码和讲师的思路,在后来你会愈来愈看不懂,反而浪费大量时间从新回头看。
因此我认为最好是先听一遍讲师讲的内容,理清思路,而后再动手写代码,这样效率会比较高,在未来出现的问题也会更少。
下面是我近1个半月以来收集的比较好的React Native入门资料和博客,分享给你们:
由于接触React Native的开发时间还不到2个月,因此有些地方不免理解的不够透彻或者理解有误,但愿发现问题的同窗多多批评或提出宝贵的建议~
本文已经同步到个人我的博客:从一个实战项目来看一下React Native开发的几个关键技术点
欢迎来参观 ^^
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。
由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。
并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~