React Native入門-實戰解析(上)

黃文臣發表於2016-04-21

來自Leo的原創部落格,轉載請著名出處

我的stackoverflow
profile for Leo on Stack Exchange, a network of free, community-driven Q&A sites


概述

隨著app端越來越複雜,迭代越來越快,很多app採用原生+html5的方式來實現,然後不知道什麼時候,它就有了個高大上的名字 - hybrid app。類似的框架也很多,比較有名的有

這種app的原理是,用webview來實現類似原生的介面,也就是用h5寫的程式碼是執行在webview裡的。優點很明顯

  • 動態部署(不需要每次通過應用商店的稽核,尤其是iOS,稽核有時候真的覺得兩顆蛋不夠疼的。)
  • 介面靈活(用h5來寫的介面要比原生程式碼靈活的多)
  • 開發迅速(一套程式碼,iOS,安卓都可以跑;不管是安卓還是iOS,用原生的程式碼實現類似webview的頁面,佈局什麼的都是很複雜的一件事情)

同樣,缺點也很明顯

- 效能相對較差(這一點在安卓上尤其明顯,安卓的webview效能真的很差,而且安卓機型較多)
- 資料處理能力較差,跑在webview裡的程式,而且JavaScript是單執行緒的,所以很難利用好多核多執行緒,
- 和硬體組建的互動較差,例如相簿,藍芽,motion等。


React Native

React native是facebook公司推出的一個app開發框架,facebook也有很多其他的框架,在這裡都可以找到。

使用純react native開發的時候,實際的開發語言是JavaScript和框架React。寫一套程式碼,同時執行在多個平臺上。

和上文提到的hybrid app最大的區別是

用React Native開發的時候,實際執行的都是原生的程式碼

目前React Native支援

  • >=Android 4.1
  • >= iOS 7.0

它的優點

  • 可以動態部署,因為是用JS寫的,並不需要編譯,所以可以在執行期動態的從伺服器下載,並且執行。
  • 開發期間,修改介面的時候,只需要Command+R來重新整理即可,不需要每次修改都蛋疼的重新編譯
  • 支援和原生程式碼混合程式設計,所以,對於那些效能要求較高,資料處理複雜的頁面,仍然可以用原生的程式碼來實現。

如果你沒有任何的JS和React基礎

可以先看看,我之前的這篇JS和React的基礎文章


本文最終的效果

本文的目的是實現一個從網路獲取資料,載入到ListView,然後點選某一行可以跳轉到詳情頁。
React Native入門-實戰解析(上)


React Native環境搭建

由於本文側重的是如何使用React Native進行開發,所以並不會詳細講解如何安裝和搭建環境。可以參考官方文件,搭建很簡單

官方文件連結

有幾點提一下

  • 最好是Mac電腦,OS X系統,因為iOS執行環境需要OSX
  • iOS目前需要XCode 7 +
  • 安卓需要Android SDK,和模擬器

文件

關於React Native的文件,在這裡你都可以找到,這個系列我不會翻譯facebook的文件。能閱讀英文文件是程式設計師的一項基本技能,但是我會在使用的時候簡單提一下


建立一個工程

開啟終端,cd到想要的目錄去,然後

react-native init LeoRNWeather

可以看到生成了一個LeoRNWeather的資料夾,這個資料夾的預設的檔案如下

android  //安卓的工程        
index.ios.js //iOS的程式入口檔案   
node_modules //
index.android.js //安卓的入口檔案  
ios         //iOS的工程
package.json //全域性的描述資訊,本文就使用預設的了

對了我使用的IDE,是Atom
然後,可以手動開啟 ios 目錄下的XCode 工程,然後點選執行,如果能見到下面截圖,代表執行成功
React Native入門-實戰解析(上)


入門

記住,React Native沒有CSS,所有的實現都是JS的語法。當你開啟index.ios.js的時候,大概能發現幾個模組

匯入的模組,要先匯入才能使用

import React, {
    ****
} from 'react-native';

樣式佈局定義,用JS的語法,由StyleSheet建立,其中樣式使用了React的FlexBox,讓佈局變的十分簡單

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的檢視語法和H5類似,標準的XML格式。

<Text style={styles.welcome}>
      Welcome to React Native!
</Text>

!!!!我們刪除這個檔案裡的全部內容,然後替換成React的風格程式碼

這時候程式碼如下

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}>這是一個標題</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);

效果

React Native入門-實戰解析(上)


新增導航欄

這裡提一下,在React Native中,導航欄有兩種

  • Navigator,大部分的情況下使用這個,由facebook的react native團隊進行開發,一直在維護,同時支援iOS和安卓,由於在導航切換的時候需要進行大量的載入,所以會佔用JS執行緒較多時間。
  • NavigatorIOS,很少使用,由開源社群開發,有很多bug,僅僅支援iOS。但是內部由原生的UINavigationController實現,所以實際執行的時候,和原生的iOS導航一樣,有一樣的動畫

本文使用NavigatorIOS,react native的相關資料還不是很多,一定要會看英文文件,NavigationIOS的文件可以在這裡找到

在頂部import引入

  NavigatorIOS,

然後,重寫LeoRNWeather,增加導航欄,其中

  • initialRoute 定義最初的頁面,類似iOS中的rootViewController,title表示標題,component表示渲染的物件,是Component的子類
var LeoRNWeather = React.createClass({
  render: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        initialRoute={{
          title: '主頁',
          component: ListScreen,
        }}
      />
    );
  }
});

建立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重新整理,可以看到效果(修改了文字)

React Native入門-實戰解析(上)


新增背景圖

首先,在目錄裡新增一張圖片
這裡寫圖片描述

Tips:當向iOS工程中Images.xcassets新增了圖片或者android新增了res/drawable新增圖片的時候,需要重新編譯

然後,將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: '主頁',
          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);

效果圖
React Native入門-實戰解析(上)

Tips
通過設定flex為1來讓寬度高度填充100%,通過height,width為null,來讓Image填充螢幕
通過設定父檢視的alignItems:'center' flexDirection:'column'來設定水平居,alignItems:'center' flexDirection:'row'來設定垂直居中

關於Flexbox佈局,可以參考這片文章,寫的非常詳細


進行網路請求

React Native網路請求的文件可以在這裡找到,在React中,網路請求使用Fetch
例如,你可以這樣去呼叫一個POST請求

fetch('https://mywebsite.com/endpoint/', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    firstParam: 'yourValue',
    secondParam: 'yourOtherValue',
  })
})

由於網路請求是一個非同步的請求,所以,它返回的是一個Promise物件,對於這個物件,有兩種處理方式

  • 同步處理then和catch
  • 非同步處理,async/await

還有一個API是XMLHttpRequest,它是建立在iOS網路請求api之上的,本文不做討論。

由於本文是這個React Native系列的第一篇,所以處理方式採用同步處理。簡單直接。
在類ListScreen中,新增如下兩個方法

  //Component掛載完畢後呼叫
  componentDidMount() {
    this.fetchData();
  },
  fetchData() {
   fetch(REQUEST_URL)
     .then((response) => response.json())
     .then((responseData) => {

     })
     .done();
  },

這裡的REQUEST_URL是一個全域性變數

var REQUEST_URL = 'https://raw.githubusercontent.com/LeoMobileDeveloper/React-Native-Files/master/person.json';

然後,save,command+R重新整理模擬器,會發現Log如下

2016-04-21 13:53:49.563 [info][tid:com.facebook.React.JavaScript] [ { nickname: 'Leo', realname: 'WenchenHuang' },
  { nickname: 'Jack', realname: 'SomethingElse' } ]

為了顯示到ListView中,我們要把網路請求來的資料儲存下來,為ListScreen新增如下方法

  • 用loaded來判斷網路資料是否載入完畢
  • 用users來儲存實際的的網路資料,這裡因為users是ListView的dataSource,所以用ListView的DataSource來初始化
  //自動呼叫一次,用來設定this.state的初始狀態
  getInitialState: function() {
    return {
      loaded: false,
      users: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }),
    };
  },

然後修改fetchData方法,在載入完畢後儲存資料

  fetchData() {
   fetch(REQUEST_URL)
     .then((response) => response.json())
     .then((responseData) => {
       this.setState({
         users: this.state.users.cloneWithRows(responseData),
         loaded: true,
       });
     })
     .done();
  },

Tips:this.setState會觸發render重新呼叫,進行重繪


寫出一個列表

移動開發中,列表是一個非常常用的控制元件。(iOS中的Tableview,android中的listview)。

ListView的文件連結

ListView的優點是,當檢視離開螢幕的時候,會被複用或者移除,降低記憶體使用。關於ListVIew,ReactNative團隊進行了很多優化,比如event-loop只渲染一個cell,將渲染工作分成很多個小的碎片執行,來防止掉幀。

如何使用ListView
最少需要以下兩個

  • dataSource,一個簡單陣列來描述MVC中的model,類似於iOS中的dataSource
  • renderRow,返回一個檢視組建.類似於iOS中的cellForRowAtIndexPath
  • renderSeparator,一般也需要這個方法,來說生成一個分隔線

當然,listView也支援很多,比如像iOS那樣的section header,header,footer,以及很多的事件回撥,在listView的文件裡,你都可以找到。

這時候的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',
  },
});

這時候,save,command+R後,發現再網路請求的時候會先顯示小菊花轉轉轉,然後載入完畢之後,顯示一個List
React Native入門-實戰解析(上)


載入Spinner(僅適用於iOS)

這個在上面的程式碼中提到了

renderLoadingView() {
    return (
      <Image source={require('./img/background.png')} style={styles.backgroundLoading}>
      <ActivityIndicatorIOS
        style={[styles.centering, {height: 80}]} //風格
        size="large" //大小
        color="#ffffff" //顏色
       />
      </Image>
    );
  },

控制檯列印

上文的程式碼裡提到

   rowClicked(user){
    console.log(user);
   },

使用console.log來實現控制檯

這時候,我們在點選某一行,會看到XCode中輸出

在Chrome中除錯

使用Command+control+Z來調出除錯視窗,然後選擇Debug in chrome

這時候,App會和Chrome建立一個socket連線,這樣在Chrome中就可以進行除錯了。

開啟Chrome開發者工具

React Native入門-實戰解析(上)
點選某一行,就會發現在chrome的控制檯進行log了


新增一個詳情頁,並且傳值

新建一個Component來表示詳情頁

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中,跳轉到詳情頁

  rowClicked(user){
    console.log(user);
    this.props.navigator.push({
      title: "詳情頁",
      component: DetailScreen,
      passProps: {user:user},
    });
  },

Tips:

  • NavigatorIOS可以通過this.props.navigator來訪問
  • 通過 this.props.navigator.push來跳轉,通過passProps: {user:user}來傳遞值
  • 每個Component的類都有兩個全獨享,this.props表示引數,this.state表示當前的狀態。可以用來儲存和傳遞資料

簡單提一下React Native的效能

在RN中,主要有兩個執行緒

  • JavaScript執行緒
  • 主執行緒(UI執行緒)

其中,JavaScript是React Native的JS程式碼執行執行緒,React Native的觸控處理,網路請求,檢視配置,以及app的業務邏輯都是發生在這裡的。主執行緒是實際原生程式碼繪製檢視的執行執行緒。使用React Native的時候,往往會遇到JavaScript執行緒執行邏輯過多,沒有辦法及時響應UI執行緒,導致掉幀.所以,React Native的效能,較純原生的還是要差一些的


後續

React Native實戰解析(中)會繼續講解一些基礎控制元件的適用,然後也會寫一個demo的app,React Native實戰解析(下)會寫一個相對完善點的應用,作為這個入門系列的終結。然後,計劃寫一兩篇混合程式設計的,最近比較忙,這個系列慢慢更新吧


附錄,最終的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: "詳情頁",
      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: '主頁',
          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);

相關文章