在進行了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日更新 ----------------------------
注意注意!!!
筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。
由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。
並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~
掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~