概述
隨著app端越來(lái)越復(fù)雜,迭代越來(lái)越快,很多app采用原生+html5的方式來(lái)實(shí)現(xiàn),然后不知道什么時(shí)候,它就有了個(gè)高大上的名字 - hybrid app。類似的框架也很多,比較有名的有
這種app的原理是,用webview來(lái)實(shí)現(xiàn)類似原生的界面,也就是用h5寫的代碼是運(yùn)行在webview里的。優(yōu)點(diǎn)很明顯
- 動(dòng)態(tài)部署(不需要每次通過應(yīng)用商店的審核,尤其是iOS,審核有時(shí)候真的覺得兩顆蛋不夠疼的。)
- 界面靈活(用h5來(lái)寫的界面要比原生代碼靈活的多)
- 開發(fā)迅速(一套代碼,iOS,安卓都可以跑;不管是安卓還是iOS,用原生的代碼實(shí)現(xiàn)類似webview的頁(yè)面,布局什么的都是很復(fù)雜的一件事情)
同樣,缺點(diǎn)也很明顯
- 性能相對(duì)較差(這一點(diǎn)在安卓上尤其明顯,安卓的webview性能真的很差,而且安卓機(jī)型較多)
- 數(shù)據(jù)處理能力較差,跑在webview里的程序,而且JavaScript是單線程的,所以很難利用好多核多線程,
- 和硬件組建的交互較差,例如相冊(cè),藍(lán)牙,motion等。
React Native
React native是facebook公司推出的一個(gè)app開發(fā)框架,facebook也有很多其他的框架,在這里都可以找到。
使用純r(jià)eact native開發(fā)的時(shí)候,實(shí)際的開發(fā)語(yǔ)言是JavaScript和框架React。寫一套代碼,同時(shí)運(yùn)行在多個(gè)平臺(tái)上。
和上文提到的hybrid app最大的區(qū)別是
用Reactive Native開發(fā)的時(shí)候,實(shí)際運(yùn)行的都是原生的代碼
目前React Native支持
- Android 4.1
- iOS 7.0
它的優(yōu)點(diǎn)
- 可以動(dòng)態(tài)部署,因?yàn)槭怯肑S寫的,并不需要編譯,所以可以在運(yùn)行期動(dòng)態(tài)的從服務(wù)器下載,并且執(zhí)行。
- 開發(fā)期間,修改界面的時(shí)候,只需要Command+R來(lái)刷新即可,不需要每次修改都蛋疼的重新編譯
- 支持和原生代碼混合編程,所以,對(duì)于那些性能要求較高,數(shù)據(jù)處理復(fù)雜的頁(yè)面,仍然可以用原生的代碼來(lái)實(shí)現(xiàn)。
本文最終的效果
本文的目的是實(shí)現(xiàn)一個(gè)從網(wǎng)絡(luò)獲取數(shù)據(jù),加載到ListView,然后點(diǎn)擊某一行可以跳轉(zhuǎn)到詳情頁(yè)。

React Native環(huán)境搭建
由于本文側(cè)重的是如何使用React Native進(jìn)行開發(fā),所以并不會(huì)詳細(xì)講解如何安裝和搭建環(huán)境??梢詤⒖脊俜轿臋n,搭建很簡(jiǎn)單
有幾點(diǎn)提一下
- 最好是Mac電腦,OS X系統(tǒng),因?yàn)閕OS運(yùn)行環(huán)境需要OSX
- iOS目前需要XCode 7 +
- 安卓需要Android SDK,和模擬器
文檔
關(guān)于React Native的文檔,在這里你都可以找到,這個(gè)系列我不會(huì)翻譯facebook的文檔。能閱讀英文文檔是程序員的一項(xiàng)基本技能,但是我會(huì)在使用的時(shí)候簡(jiǎn)單提一下
創(chuàng)建一個(gè)工程
打開終端,cd到想要的目錄去,然后
react-native init LeoRNWeather
可以看到生成了一個(gè)LeoRNWeather的文件夾,這個(gè)文件夾的默認(rèn)的文件如下
android //安卓的工程
index.ios.js //iOS的程序入口文件
node_modules //
index.android.js //安卓的入口文件
ios //iOS的工程
package.json //全局的描述信息,本文就使用默認(rèn)的了
對(duì)了我使用的IDE,是Atom
然后,可以手動(dòng)打開 ios 目錄下的XCode 工程,然后點(diǎn)擊運(yùn)行,如果能見到下面截圖,代表運(yùn)行成功

入門
記住,React Native沒有CSS,所有的實(shí)現(xiàn)都是JS的語(yǔ)法。當(dāng)你打開index.ios.js的時(shí)候,大概能發(fā)現(xiàn)幾個(gè)模塊
導(dǎo)入的模塊,要先導(dǎo)入才能使用
import React, {
****
} from 'react-native';
樣式布局定義,用JS的語(yǔ)法,由StyleSheet創(chuàng)建,其中樣式使用了React的FlexBox,讓布局變的十分簡(jiǎn)單
const styles = StyleSheet.create({
//*
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
//*
});
視圖組件,視圖繼承自Component,可以在文檔上找到很多Components
class LeoRNWeather extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
</View>
);
}
}
可以看看這一行,可以看到React的視圖語(yǔ)法和H5類似,標(biāo)準(zhǔn)的XML格式。
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
?。。?!我們刪除這個(gè)文件里的全部?jī)?nèi)容,然后替換成React的風(fēng)格代碼
這時(shí)候代碼如下
import React, {
AppRegistry,
Component,
StyleSheet,
View,
ListView,
Text,
} from 'react-native';
var LeoRNWeather = React.createClass({
render(){
return (
<View style= {styles.container}>
<Text style={styles.blackText}>這是一個(gè)標(biāo)題</Text>
</View>
);
}
});
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
},
blackText:{
fontSize:20,
color:'rgb(0,0,0)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'center',
marginLeft:10,
},
});
AppRegistry.registerComponent('LeoRNWeather', () => LeoRNWeather);
效果

添加導(dǎo)航欄
這里提一下,在React Native中,導(dǎo)航欄有兩種
- Navigator,大部分的情況下使用這個(gè),由facebook的react native團(tuán)隊(duì)進(jìn)行開發(fā),一直在維護(hù),同時(shí)支持iOS和安卓,由于在導(dǎo)航切換的時(shí)候需要進(jìn)行大量的加載,所以會(huì)占用JS線程較多時(shí)間。
- NavigatorIOS,很少使用,由開源社區(qū)開發(fā),有很多bug,僅僅支持iOS。但是內(nèi)部由原生的- UINavigationController實(shí)現(xiàn),所以實(shí)際運(yùn)行的時(shí)候,和原生的iOS導(dǎo)航一樣,有一樣的動(dòng)畫
本文使用NavigatorIOS,react native的相關(guān)資料還不是很多,一定要會(huì)看英文文檔,NavigationIOS的文檔可以在這里找到
在頂部import引入
NavigatorIOS,
然后,重寫LeoRNWeather,增加導(dǎo)航欄,其中
- initialRoute 定義最初的頁(yè)面,類似iOS中的rootViewController,title表示標(biāo)題,component表示渲染的對(duì)象,是Component的子類
var LeoRNWeather = React.createClass({
render: function() {
return (
<NavigatorIOS
style={styles.container}
initialRoute={{
title: '主頁(yè)',
component: ListScreen,
}}
/>
);
}
});
創(chuàng)建ListScreen
var ListScreen = React.createClass({
render(){
return (
<View style= {styles.container}>
<Text style={styles.blackText}>blog.csdn.net/hello_hwc</Text>
</View>
);
}
});
然后, Save,選擇模擬器,command+R刷新,可以看到效果(修改了文字)

添加背景圖
首先,在目錄里添加一張圖片

Tips:當(dāng)向iOS工程中Images.xcassets添加了圖片或者android添加了res/drawable添加圖片的時(shí)候,需要重新編譯
然后,將index.ios.js修改成如下
import React, {
AppRegistry,
Component,
StyleSheet,
View,
ListView,
Text,
NavigatorIOS,
Image,
} from 'react-native';
var ListScreen = React.createClass({
render(){
return (
<Image source={require('./img/background.png')} style={styles.backgroundImg}>
<Text style={styles.whiteText}>blog.csdn.net/hello_hwc</Text>
</Image>
);
}
});
var LeoRNWeather = React.createClass({
render: function() {
return (
<NavigatorIOS
style={styles.container}
initialRoute={{
title: '主頁(yè)',
component: ListScreen,
}}
/>
);
}
});
const styles = StyleSheet.create({
backgroundImg:{
flex:1,
width: null,
height: null,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
whiteText:{
fontSize:20,
color:'rgb(255,255,255)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'left',
marginLeft:10,
},
container: {
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
},
blackText:{
fontSize:20,
color:'rgb(0,0,0)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'center',
marginLeft:10,
},
});
AppRegistry.registerComponent('LeoRNWeather', () => LeoRNWeather);
效果圖

Tips
通過設(shè)置flex為1來(lái)讓寬度高度填充100%,通過height,width為null,來(lái)讓Image填充屏幕
通過設(shè)置父視圖的alignItems:'center' flexDirection:'column'來(lái)設(shè)置水平居,alignItems:'center' flexDirection:'row'來(lái)設(shè)置垂直居中
關(guān)于Flexbox布局,可以參考這片文章,寫的非常詳細(xì)
進(jìn)行網(wǎng)絡(luò)請(qǐng)求
React Native網(wǎng)絡(luò)請(qǐng)求的文檔可以在這里找到,在React中,網(wǎng)絡(luò)請(qǐng)求使用Fetch,
例如,你可以這樣去調(diào)用一個(gè)POST請(qǐng)求
fetch('https://mywebsite.com/endpoint/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstParam: 'yourValue',
secondParam: 'yourOtherValue',
})
})
由于網(wǎng)絡(luò)請(qǐng)求是一個(gè)異步的請(qǐng)求,所以,它返回的是一個(gè)Promise對(duì)象,對(duì)于這個(gè)對(duì)象,有兩種處理方式
- 同步處理then和catch
- 異步處理,async/await
還有一個(gè)API是XMLHttpRequest,它是建立在iOS網(wǎng)絡(luò)請(qǐng)求api之上的,本文不做討論。
由于本文是這個(gè)React Native系列的第一篇,所以處理方式采用同步處理。簡(jiǎn)單直接。
在類ListScreen中,添加如下兩個(gè)方法
//Component掛載完畢后調(diào)用
componentDidMount() {
this.fetchData();
},
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
})
.done();
},
這里的REQUEST_URL是一個(gè)全局變量
var REQUEST_URL = 'https://raw.githubusercontent.com/LeoMobileDeveloper/React-Native-Files/master/person.json';
然后,save,command+R刷新模擬器,會(huì)發(fā)現(xiàn)Log如下
2016-04-21 13:53:49.563 [info][tid:com.facebook.React.JavaScript] [ { nickname: 'Leo', realname: 'WenchenHuang' },
{ nickname: 'Jack', realname: 'SomethingElse' } ]
為了顯示到ListView中,我們要把網(wǎng)絡(luò)請(qǐng)求來(lái)的數(shù)據(jù)存儲(chǔ)下來(lái),為L(zhǎng)istScreen添加如下方法
- 用loaded來(lái)判斷網(wǎng)絡(luò)數(shù)據(jù)是否加載完畢
- 用users來(lái)存儲(chǔ)實(shí)際的的網(wǎng)絡(luò)數(shù)據(jù),這里因?yàn)閡sers是ListView的dataSource,所以用ListView的DataSource來(lái)初始化
//自動(dòng)調(diào)用一次,用來(lái)設(shè)置this.state的初始狀態(tài)
getInitialState: function() {
return {
loaded: false,
users: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
};
},
然后修改fetchData方法,在加載完畢后保存數(shù)據(jù)
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
users: this.state.users.cloneWithRows(responseData),
loaded: true,
});
})
.done();
},
Tips:this.setState會(huì)觸發(fā)render重新調(diào)用,進(jìn)行重繪
寫出一個(gè)列表
移動(dòng)開發(fā)中,列表是一個(gè)非常常用的控件。(iOS中的Tableview,android中的listview)。
ListView的優(yōu)點(diǎn)是,當(dāng)視圖離開屏幕的時(shí)候,會(huì)被復(fù)用或者移除,降低內(nèi)存使用。關(guān)于ListVIew,ReactNative團(tuán)隊(duì)進(jìn)行了很多優(yōu)化,比如event-loop只渲染一個(gè)cell,將渲染工作分成很多個(gè)小的碎片執(zhí)行,來(lái)防止掉幀。
如何使用ListView
最少需要以下兩個(gè)
- dataSource,一個(gè)簡(jiǎn)單數(shù)組來(lái)描述MVC中的model,類似于iOS中的dataSource
- renderRow,返回一個(gè)視圖組建.類似于iOS中的cellForRowAtIndexPath
- renderSeparator,一般也需要這個(gè)方法,來(lái)說(shuō)生成一個(gè)分隔線
當(dāng)然,listView也支持很多,比如像iOS那樣的section header,header,footer,以及很多的事件回調(diào),在listView的文檔里,你都可以找到。
這時(shí)候的ListScreen類如下
var ListScreen = React.createClass({
getInitialState: function() {
return {
loaded: false,
users: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
};
},
componentDidMount() {
this.fetchData();
},
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
users: this.state.users.cloneWithRows(responseData),
loaded: true,
});
})
.done();
},
render(){
if (!this.state.loaded) {
return this.renderLoadingView()
}
return this.renderList()
},
renderLoadingView() {
return (
<Image source={require('./img/background.png')} style={styles.backgroundLoading}>
<ActivityIndicatorIOS
style={[styles.centering, {height: 80}]}
size="large"
color="#ffffff"
/>
</Image>
);
},
renderList(){
return (
<Image source={require('./img/background.png')} style={styles.backgroundImg}>
<ListView
dataSource={this.state.users}
renderRow={this.renderRow}
style={styles.fullList}
renderSeparator={(sectionID, rowID) => <View key={`${sectionID}-${rowID}`} style={styles.separator} />}
/>
</Image>
);
},
renderRow(user){
return (
<TouchableHighlight
onPress={() => this.rowClicked(user)}
underlayColor = '#ddd'>
<View style={styles.rightCongtainer}>
<Text style={styles.whiteText}>{user.nickname}</Text>
<Text style={styles.whiteText}>{user.realname}</Text>
</View>
</TouchableHighlight>
);
},
rowClicked(user){
console.log(user);
},
});
Styles如下
const styles = StyleSheet.create({
backgroundImg:{
flex:1,
width: null,
height: null,
flexDirection: 'row'
},
backgroundLoading:{
flex:1,
width: null,
height: null,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
thumbnail: {
width: 60,
height: 60,
},
rightCongtainer:{
flex:1,
},
fullList:{
flex:1,
paddingTop: 64,
},
separator: {
height: 0.5,
backgroundColor: 'rgba(255,255,255,0.5)',
},
centering: {
alignItems: 'center',
justifyContent: 'center',
},
whiteText:{
fontSize:20,
color:'rgb(255,255,255)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'left',
marginLeft:10,
},
blackText:{
fontSize:20,
color:'rgb(0,0,0)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'center',
marginLeft:10,
},
container: {
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
},
});
這時(shí)候,save,command+R后,發(fā)現(xiàn)再網(wǎng)絡(luò)請(qǐng)求的時(shí)候會(huì)先顯示小菊花轉(zhuǎn)轉(zhuǎn)轉(zhuǎn),然后加載完畢之后,顯示一個(gè)List

加載Spinner(僅適用于iOS)
這個(gè)在上面的代碼中提到了
renderLoadingView() {
return (
<Image source={require('./img/background.png')} style={styles.backgroundLoading}>
<ActivityIndicatorIOS
style={[styles.centering, {height: 80}]} //風(fēng)格
size="large" //大小
color="#ffffff" //顏色
/>
</Image>
);
},
控制臺(tái)打印
上文的代碼里提到
rowClicked(user){
console.log(user);
},
使用console.log來(lái)實(shí)現(xiàn)控制臺(tái)
這時(shí)候,我們?cè)邳c(diǎn)擊某一行,會(huì)看到XCode中輸出
在Chrome中調(diào)試
使用Command+control+Z來(lái)調(diào)出調(diào)試窗口,然后選擇Debug in chrome
這時(shí)候,App會(huì)和Chrome建立一個(gè)socket連接,這樣在Chrome中就可以進(jìn)行調(diào)試了。
打開Chrome開發(fā)者工具

點(diǎn)擊某一行,就會(huì)發(fā)現(xiàn)在chrome的控制臺(tái)進(jìn)行l(wèi)og了
添加一個(gè)詳情頁(yè),并且傳值
新建一個(gè)Component來(lái)表示詳情頁(yè)
var DetailScreen = React.createClass({
render(){
return (
<View style= {styles.container}>
<Text style={styles.blackText}>{this.props.user.nickname}</Text>
<Text style={styles.blackText}>{this.props.user.realname}</Text>
</View>
);
}
});
然后,在rowClick中,跳轉(zhuǎn)到詳情頁(yè)
rowClicked(user){
console.log(user);
this.props.navigator.push({
title: "詳情頁(yè)",
component: DetailScreen,
passProps: {user:user},
});
},
Tips:
- NavigatorIOS可以通過this.props.navigator來(lái)訪問
- 通過 this.props.navigator.push來(lái)跳轉(zhuǎn),通過passProps: {user:user}來(lái)傳遞值
- 每個(gè)Component的類都有兩個(gè)全獨(dú)享,this.props表示參數(shù),this.state表示當(dāng)前的狀態(tài)??梢杂脕?lái)存儲(chǔ)和傳遞數(shù)據(jù)
簡(jiǎn)單提一下React Native的性能
在RN中,主要有兩個(gè)線程
- JavaScript線程
- 主線程(UI線程)
其中,JavaScript是React Native的JS代碼執(zhí)行線程,React Native的觸摸處理,網(wǎng)絡(luò)請(qǐng)求,視圖配置,以及app的業(yè)務(wù)邏輯都是發(fā)生在這里的。主線程是實(shí)際原生代碼繪制視圖的執(zhí)行線程。使用React Native的時(shí)候,往往會(huì)遇到JavaScript線程執(zhí)行邏輯過多,沒有辦法及時(shí)響應(yīng)UI線程,導(dǎo)致掉幀.所以,React Native的性能,較純?cè)倪€是要差一些的
后續(xù)
Reactive Native實(shí)戰(zhàn)解析(中)會(huì)繼續(xù)講解一些基礎(chǔ)控件的適用,然后也會(huì)寫一個(gè)demo的app,Reactive Native實(shí)戰(zhàn)解析(下)會(huì)寫一個(gè)相對(duì)完善點(diǎn)的應(yīng)用,作為這個(gè)入門系列的終結(jié)。
附錄,最終的index.ios.js全部代碼
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
import React, {
AppRegistry,
Component,
StyleSheet,
ListView,
Text,
View,
Image,
ActivityIndicatorIOS,
Navigator,
TouchableHighlight,
TouchableOpacity,
NavigatorIOS,
} from 'react-native';
var REQUEST_URL = 'https://raw.githubusercontent.com/LeoMobileDeveloper/React-Native-Files/master/person.json';
var ListScreen = React.createClass({
getInitialState: function() {
return {
loaded: false,
users: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
};
},
componentDidMount() {
this.fetchData();
},
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
users: this.state.users.cloneWithRows(responseData),
loaded: true,
});
})
.done();
},
render(){
if (!this.state.loaded) {
return this.renderLoadingView()
}
return this.renderList()
},
renderLoadingView() {
return (
<Image source={require('./img/background.png')} style={styles.backgroundLoading}>
<ActivityIndicatorIOS
style={[styles.centering, {height: 80}]}
size="large"
color="#ffffff"
/>
</Image>
);
},
renderList(){
return (
<Image source={require('./img/background.png')} style={styles.backgroundImg}>
<ListView
dataSource={this.state.users}
renderRow={this.renderRow}
style={styles.fullList}
renderSeparator={(sectionID, rowID) => <View key={`${sectionID}-${rowID}`} style={styles.separator} />}
/>
</Image>
);
},
renderRow(user){
return (
<TouchableHighlight
onPress={() => this.rowClicked(user)}
underlayColor = '#ddd'>
<View style={styles.rightCongtainer}>
<Text style={styles.whiteText}>{user.nickname}</Text>
<Text style={styles.whiteText}>{user.realname}</Text>
</View>
</TouchableHighlight>
);
},
rowClicked(user){
console.log(user);
this.props.navigator.push({
title: "詳情頁(yè)",
component: DetailScreen,
passProps: {user:user},
});
},
});
var DetailScreen = React.createClass({
render(){
return (
<View style= {styles.container}>
<Text style={styles.blackText}>{this.props.user.nickname}</Text>
<Text style={styles.blackText}>{this.props.user.realname}</Text>
</View>
);
}
});
var LeoRNWeather = React.createClass({
render: function() {
return (
<NavigatorIOS
style={styles.container}
initialRoute={{
title: '主頁(yè)',
component: ListScreen,
}}
/>
);
}
});
const styles = StyleSheet.create({
backgroundImg:{
flex:1,
width: null,
height: null,
flexDirection: 'row'
},
backgroundLoading:{
flex:1,
width: null,
height: null,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
thumbnail: {
width: 60,
height: 60,
},
rightCongtainer:{
flex:1,
},
fullList:{
flex:1,
paddingTop: 64,
},
separator: {
height: 0.5,
backgroundColor: 'rgba(255,255,255,0.5)',
},
centering: {
alignItems: 'center',
justifyContent: 'center',
},
whiteText:{
fontSize:20,
color:'rgb(255,255,255)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'left',
marginLeft:10,
},
blackText:{
fontSize:20,
color:'rgb(0,0,0)',
backgroundColor:'rgba(255,255,255,0)',
textAlign:'center',
marginLeft:10,
},
container: {
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
},
});
AppRegistry.registerComponent('LeoRNWeather', () => LeoRNWeather);