從一個實戰專案來看一下React Native開發的幾個關鍵技術點

J_Knight_發表於2017-08-30

在進行了2個星期的基礎學習(Flexbox, React.js, JSX, JavaScript)之後,想通過一個實戰專案來提高React Native的開發水平,於是找到了下面這個專案:

一. 專案介紹

這是我在學習賈鵬輝老師在慕課網上的一個很火的React Native實戰的教程後,寫出的課程Demo。該課程是慕課網裡很火的一個React Native課程,當初在看了課程介紹和課程安排覺得講解的點還是很全的,所以毫不猶豫地買了下來。

從看視訊,敲程式碼到重構,改bug,大概花了2個多星期的時間,除了呼叫友盟的SDK以及CodePush整合之外,其他的部分都基本完成了,JavaScript程式碼佔據了95%,基本上算是一個純React Native專案,而且同時可以在iOS和Android裝置上執行:

上排是iOS模擬器 | 下排是Android模擬器

而且比較吸引人的是該專案可以實現多個主題的切換:

多主題切換

主題切換的技術實現會在下文給出。

用一個動圖來過一遍大致的需求:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

Demo GitHub地址:GitHubPopular-SJ 可以按照README檔案裡的方法執行該專案。

上傳到GitHub已經過賈老師允許

值得一提的是:這確實是一門物有所值的課程,可以讓想入門React Native的開發者少走很多彎路。雖然我上傳的Demo可以實現視訊裡大部分功能,但是經過除錯,修改後的程式碼資訊量還是很有限的,而且老師在視訊中講解的很多關於實際開發的知識點在程式碼中並沒有體現出來,所以還是建議各位報名參加課程來提高自己的開發水平。

二. React Native開發的幾個關鍵技術點

首先用一張思維導圖來看一下第二節講的內容:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

2.1 元件化的思想

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):每個小的元件僅僅包含自身的邏輯,更容易被理解和維護;

舉個?,我們看一下這個Demo使用的導航欄:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

封裝好的導航欄就可以被稱之為一個元件,它符合上述三個特點:

  1. 可組合:可以將導航欄元件放在頁面元件中作為頁面元件的子元件。而且在導航欄元件的內部,也有按鈕元件等子元件。
  2. 可重用:如果封裝好了該元件,就可以放在任意需要導航欄的頁面(元件)使用,也可以放在其他專案中使用。
  3. 可維護:因為具有獨立的功能和展示邏輯,所以便於定位和修改。

在瞭解了元件的基本概念以後,我們來看一下元件其他的一些相關知識。

2.2 元件的屬性與狀態

在React Native(React.js)裡,元件所持有的資料分為兩種:

  1. 屬性(props):元件的props是不可變的,它只能從其他的元件(例如父元件)傳遞過來。
  2. 狀態(state):元件的state是可變的,它負責處理與使用者的互動。在通過使用者點選事件等操作以後,如果使得當前元件的某個state發生了改變,那麼當前元件就會觸發render()方法重新整理自己。

舉一個這個專案的收藏頁面來說:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

我們可以看到這個頁面有兩個子頁面,一個是‘最熱’頁面(元件),另一個是‘趨勢‘頁面(元件)。那麼這兩個元件都有什麼props和state呢?

props

首先看一下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的不同:tabLabelflag

而在FavoriteTabPage元件內部,如果想呼叫flag這個props,可以使用this.props.flag來呼叫。

再來看一下state:

state

下面是最熱和趨勢頁面的元件:

class FavoriteTabPage extends Component{

    //元件的構造方法
    constructor(props){

        super(props);
        this.state={
            dataSource:new ListView.DataSource({rowHasChanged:(r1,r2)=>r1!==r2}),
            isLoading:false,
        }
    }
    ...
}
複製程式碼

這裡面定義了兩個state:

  1. dataSource:列表的資料來源
  2. isLoading:是否正在重新整理

這兩個state都是將來可能經常變化的。比如在網路請求以後,列表的資料來源會被替換掉,這個時候就要呼叫

 this.setState({
      //把新的值newDataArr物件傳給dataSource
      dataSource:newDataArr
 })
複製程式碼

來觸發render()方法來重新整理列表元件。

2.3 元件的生命週期

和iOS開發裡ViewController的生命週期類似,元件也有生命週期,大致分為三大階段:

  • Mounting:已插入真實 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真實 DOM

DOM是前端的一個概念,暫時可以粗略理解為一個頁面的樹形結構。

在每個階段都有相應的狀態和與之對應的回撥函式,具體可以看下圖:

元件的生命週期

上圖來自:賈鵬輝的技術部落格:React Native之React速學教程(中)

從上圖中我們可以看到,React 為每個狀態都提供了兩種回撥函式,will 函式在進入狀態之前呼叫,did 函式在進入狀態之後呼叫。

在這裡講一下這其中幾個重要的回撥函式:

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 中移除的時候立刻被呼叫。例如當前頁面點選返回鍵跳轉到上一頁面的時候就會呼叫。

我們通常在這個方法裡移除通知。具體做法在後文會提到。

到此,已經講解了一些元件相關的知識,下面來看一下我們如何使用元件來搭建介面。

2.4 使用元件來搭建介面

在這裡我們舉幾個例子來看一下在React Native裡搭建View的方式。

首先我們來看一下最熱頁面的cell是如何佈局的:

2.41 搭建cell元件

首先舉一個在最熱標籤頁面列表裡的一個cell為例,講解一下一個簡單的UI元件是如何實現的:

最熱標籤頁面的cell

我們把該元件定名為: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,也就是元件類,即是說,宣告元件的時候必須都要繼承與這個類。
  • 集中看一下該元件的render方法,它返回的是該元件的實際佈局:在語法上使用JSX,類似於HTML的標籤式語法,很清楚地將cell的層級展現了出來:   - 最外層被一個View元件包裹著,裡面第一層有三個子元件:兩個Text元件和一個作為底部背景的View元件。   - 底部背景的View元件又有三個子元件:View元件(顯示作者資訊),View元件(顯示star資訊),收藏按鈕。

試著結合程式碼來看一下下面的圖片,可以看出元件的實際佈局與程式碼的佈局是高度一致的:

Cell 佈局

然而僅僅定義元件的層級關係是不夠的,我們還需要定義元件的樣式(例如圖片元件的大小樣式等等),這時候就通過定義一個樣式的物件(通常使用常量物件)來定義一些需要使用的樣式:

//樣式常量
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'',有圖有真相:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

這是初學者比較常犯的錯誤,所以要注意:在搭建頁面的時候,一定要區分是物件還是常量。如果是物件就必須要用大括號括起來!如果是物件就必須要用大括號括起來!如果是物件就必須要用大括號括起來!

這裡每個樣式裡面的長,寬,內外邊距,以及flexDirection等flexBox相關的佈局屬性就不介紹了。可以通過查詢本文最後的相關連結來學習。

2.42 搭建靜態表格頁

在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'
    }
});
複製程式碼

在上面的程式碼中,我們可以看到一個頁面元件的全貌,它大致分為三個區域:

  1. 引用區域
  2. 定義元件區域
  3. 定義樣式區域

下面兩個區域在上一節已經介紹過。第一個區域,引用區域一般寫在元件檔案的開頭,在這裡一般是需要引入該元件需要的其他元件或者常量。

現在看一下該元件的render()函式,它返回了用來包裹整個頁面的View元件,該元件有兩個子元件

  • NavigationBar元件(導航欄),傳入了兩個props:title和style。
  • ScrollView元件,包裹了專案資訊Cell的View元件,分割線,專案Cell的View元件。需要注意的是,每個cell的元件都比較類似,所以在這裡將生成它的程式碼封裝起來做一個函式來呼叫:
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個引數:

  • callback:點選cell時呼叫的方法,需要父元件傳入
  • icon:cell左側的圖片
  • text:cell標題
  • tintColor:cell的主題顏色
  • expandableIcon:cell右側的圖片(三角箭頭)

因為在React Native中沒有特定的Button元件,所以實現元件的點選都是通過被TouchableHighlight等可點選元件包裹來實現的。

常用的可以實現點選效果的是View元件和Text元件。

注意一下TouchableHighlight裡面傳入的兩個props:

  1. 如果需要在點選時顏色不變,可以將它的underlayColor設為transparent
  2. 可以把點選時觸發的函式傳給它的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>
複製程式碼

它的樣式呼叫了GlobalStylescellBottomLineStyle。因為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>
複製程式碼

2.43 搭建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來操作,比較保險一點。

在確認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。該函式有四個引數:

  • Component:當前tab被點選後顯示的元件。
  • selectedTab:當前tab的唯一標識。
  • title:當前tab的標題。
  • renderIcon:當前tab的圖示。

_renderTab方法裡,我們返回一個TabNavigator.Item元件,除了一些關於tab的props的定義以外,我們將屬於該tab的元件填充了進去:

<Component {...this.props} theme={this.state.theme} homeComponent={this}/>
複製程式碼

在這裡,{...this.props}是將當前HomePage的所有props賦給這個Component。還有另外兩個props也定義了進去:themehomeComponent

這裡用一個常量定義了四個tab的唯一標識,需要注意的是,這個常量是可以被其他元件獲得的,以為它被export欄位修飾了。

另外,還需要注意一下HomePage有一個屬性是selectedTab,它用來標記當前選擇的tab是哪一個。在constructor方法裡做了一個判斷,如果沒有從外部元件傳進來selectedTab,則需要初始化為FLAG_TAB.flag_popularTab

2.5 元件間通訊

既然React專案是以元件為單位搭建的,那麼一定少不了元件之間的資料和事件的傳遞,也就是元件之間的通訊。

元件間通訊分為兩大類:

  1. 有直接關係或間接關係的元件之間通訊

  2. 無直接關係或間接關係的元件之間通訊

2.51 有直接關係或間接關係的元件之間通訊

我個人是這麼理解父元件和子元件的關係的:

如果A元件包含了B元件,或者說在A元件裡建立了B元件,那麼A元件就是B元件的父元件;反過來B元件就是A元件的子元件,是有直接關係的元件。

比如:

  • 一個介面的導航欄元件是整個頁面元件的子元件,因為這個導航欄元件被包含在了當前的頁面元件當中。

  • 從這個頁面跳轉到的下一個頁面是當前頁面的子元件:因為被包含在了當前頁面元件的Navigator裡。

再加上子元件和子元件的通訊,直接或間接關係元件之間的通訊就分為下面這三種情況:

  1. 父元件向子元件傳遞資料和事件。

  2. 子元件向父元件傳遞訊息和事件。

  3. 子元件向子元件傳遞訊息和事件。

父元件向子元件傳遞資料和事件:通過對子元件的屬性賦值來實現。

在上面我們看到,在給頁面佈局的時候我們使用了導航欄元件:

<NavigationBar
      title={'我的'}
      style={this.state.theme.styles.navBar}
 />
複製程式碼

在這裡,當前頁面元件將'我的'物件,以及this.state.theme.styles.navBar物件分別賦值給了導航欄元件。而導航欄接收到這兩個值以後,在其內部可以通過this.props.titlethis.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被賦值了,其中,onSelectonFavorite被賦予了函式:

  • 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.onSelectthis.props.onFavorite。這兩個函式被賦給了兩個TouchableOpacity元件的onPress裡面。這裡的()=>可以理解為為傳遞事件,表示當該控制元件被點選後的事件。

不同的是,this.props.onFavorite()是可以將兩個值回傳給其父元件。細心的同學會發現,在給RespositoryCell傳值的時候,是有兩個返回值存在的。

注意,在這裡的TouchableOpacity和上文提到的TouchableHighlight類似,都可以讓非可點選元件變成可點選元件。區別在於配合TouchableOpacity使用時,點選後無高亮效果。而TouchableHighlight預設是有高亮效果的。

OK,現在我們知道了父元件和子元件是如何傳遞資料和事件了:

  • 父元件到子元件:通過直接給屬性賦值
  • 子元件到父元件:通過父元件給子元件傳遞迴調函式

需要注意的是,上面講的都是直接關係的父子元件,其實還有間接關係的元件,也就是兩個元件之間有一個或多個元件連線著,比如父元件的子元件的子元件。這些元件之間的通訊都可以通過上述的方法來實現,只不過是中間跨過多少層的區別而已。

需要注意的是,這裡說的父元件和子元件的通訊,不僅僅包括這種直接關係,還包括間接關係,而間接關係的元件就是該元件與其子元件的子元件的關係。

所以無論中間隔了多少元件,只要是存在於這種關係鏈上的元件,都可以用上述兩種方式來傳遞資料和事件。

兄弟元件之間的通訊

雖然不是包含於被包含,由誰建立了誰的關係,但是同一父元件下的幾個子元件(兄弟元件)也算得上是有間接關係了(中間夾著共同的父元件)。

那麼在同一父元件下的兩個子元件是如何傳遞資料呢?

答案是通過二者所共享的父元件的state來傳遞資料的

因為我們知道觸發元件的渲染是通過setState方法的。因此,如果兩個子元件都使用了他們的父元件的同一個state來渲染自己。

那麼當其中一個子元件觸發了setState,更新了這個共享的父元件的state,繼而觸發了父元件的render()方法,那麼這兩個子元件都會依據這個更新後的state來重新整理自己,這樣一來,就實現了子元件的資料傳遞。

到現在就講完了有直接或間接關係的元件之間的通訊,下面來講一下無直接關係或間接關係的元件之間的通訊:

2.52 無直接關係和間接關係的元件之間通訊

如果兩個元件從屬於不同的關係鏈既沒有直接關係,也沒有間接關係(例如不同模組下的兩個頁面元件),那麼想實現通訊的話,就需要通過通知機制,或者本地持久化方案來實現。在這裡先介紹一下通知機制,而本地持久化會在下面單拿出一節來專門講解。

通知機制可以通過這個Demo的收藏功能來講解:

先大致介紹一下收藏的需求:

  1. 在最熱標籤頁或者語言趨勢頁面如果點選了收藏按鈕,那麼在收藏頁面就會增加被收藏的專案(注意,點選收藏按鈕後不進行網路請求,也就是說,收藏頁面是沒有網路請求的)。
  2. 而如果在收藏頁面中取消了收藏,就需要在最熱標籤頁面或語言趨勢頁面中對應的專案裡面更新取消收藏的效果(同樣沒有網路請求)。

因為這三個頁面從屬於不同模組, 而且又不是以網路請求的方式重新整理列表,所以如果要滿足上述需求,就需要使用通知或者本地儲存的方式來實現。

在這個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;
     })
}
複製程式碼

在這裡通過給DeviceEventEmitteraddListener方法傳入兩個引數來進行通知的註冊:

  • 第一個引數是通知的頻道,用來區別其他的通知。
  • 第二個引數是需要呼叫的函式:在這裡只是將this.isFavoriteChanged賦值為YES。它的目的是在於將來如果該值等於YES,就進行介面的再渲染,更新收藏狀態。

需要注意的是,有註冊,就要有登出,在元件被解除安裝之前,需要將監聽解除:

componentWillUnmount() {
     if(this.listener){
         this.listener.remove();
     }
}
複製程式碼

這樣,我們搞定了通知的註冊,就可以在程式的任意地方傳送通知了。在該需求中,我們需要攔截住在收藏頁面裡對專案的收藏按鈕的點選,只要點選了,就傳送通知:告知最熱標籤頁面收藏的狀態改變了:

onFavorite(item,isFavorite){

    ...
    DeviceEventEmitter.emit('favoriteChanged_popular');

}
複製程式碼

在這裡,攔截了收藏按鈕的點選。還記得麼?這裡onFavorite()函式就是上面說的點選收藏按鈕的回撥。

我們在這裡傳送了通知,只需傳入頻道名稱即可。

是不是很easy?

OK,到這裡我們講完了元件間的通訊這一塊,簡單回想一下各種關係的元件之間的通訊方案。

下面我們來講一下在React Native裡的本地持久化的方案。

2.6 本地持久化

類似於iOS 中的NSUserDefault, AsyncStorage 是React Native中的 Key-Value 儲存系統,可以做本地持久化。

首先看它主要的幾個介面:

2.61 AsyncStorage常用介面

根據鍵來獲取值,獲取的結果會放在回撥函式中:

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))
複製程式碼

設定多項,其中 keyValuePairs 是字串的二維陣列,比如:[['k1', 'val1'], ['k2', 'val2']]:

static multiSet(keyValuePairs, callback:(errors))
複製程式碼

獲取多項,其中 keys 是字串陣列,比如:['k1', 'k2']:

static multiGet(keys, callback:(errors, result))
複製程式碼

刪除多項,其中 keys 是字串陣列,比如:['k1', 'k2']:

static multiRemove(keys, callback:(errors))
複製程式碼

清除所有的專案:

static clear(callback:(error))
複製程式碼

2.62 AsyncStorage使用注意事項

需要注意的是,在使用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被解析出來後是一個陣列。

2.7 網路請求

在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,第二個引數是一個字典,包括方法,請求頭,請求體等資訊。

隨後的thencatch分別捕捉了fetch函式的返回值:一個Promise物件的正確結果錯誤結果。注意,這裡面有兩個then,其中第二個then把第一個then的結果拿了過來。而第一個then做的事情是把網路請求的結果轉化為JSON物件。

那麼什麼是Promise物件呢?

Promise 是非同步程式設計的一種解決方案,Promise物件可以獲取某個非同步操作的訊息。它裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。

它分為三種狀態:

Pending(進行中)、Resolved(已成功)和Rejected(已失敗)

它的建構函式接受一個函式作為引數,該函式的兩個引數分別是resolvereject

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)
                })
        })
    }
}
複製程式碼

2.8 離線快取

離線快取技術可以利用上文提到的FetchAsyncStorage實現,將請求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);
                 })
             })
    }
複製程式碼

2.9 主題更換

這個Demo有一個主題更換的需求,在主題設定頁點選某個顏色之後,全app的顏色方案就會改變:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

我們只需要將四個模組的第一個頁面的主題修改即可,因為第二個頁面的主題都是從第一個頁面傳進去的,所以只要第一個頁面的主題改變了即可。

但是,我們應該不能在選擇新主題之後同時向這四個頁面都傳送通知,命令它們修改自己的頁面,而是應該採取一個更加優雅的方法來解決這個問題:使用父類。

新建一個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]
        ))
    }
複製程式碼

2.10 功能除錯

我們可以使用瀏覽器的開發者工具來除錯React Native專案,可以通過打斷點的方式來看資料資訊以及方法的呼叫:

  1. 首先在iOS模擬器中點選command + D,然後再彈出選單裡點選Debug JS Remotely。隨後就開啟了瀏覽器進入了除錯。

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

  1. 瀏覽器一般會展示下面的頁面,然後點選command + option + J進入真生的除錯介面。

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

  1. 點選最上方的Sources,然後點選左側debuggerWorker.js下的localhost:8081,就可以看到目錄檔案。點選需要除錯的檔案,在行數欄就可以打斷點了。

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

2.11 適配iOS和Android平臺

因為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來區分平臺。

2.12 組織專案結構

在終端輸入react-native demo --version 0.44.0命令以後,就會初始化一個React Native版本為0.44.0的專案。這個最初專案裡面直接就包含了iOS和Android的工程資料夾,可以用對應的IDE開啟後編譯執行。

在新建一個React Native專案之後的根目錄結構是這樣的:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

或者也可以根目錄下輸入react-native run-ios或者react-native run-android指令, 就會自動開啟模擬器執行專案(前提是安裝了相應的開發環境)。

但是一個比較完整的專案僅僅有這些類別的檔案是不夠的,還需要一些工具類,模型類,資源等檔案。為了很好地區分它們,使專案結構一目瞭然,需要組織好專案資料夾以及類的命名,下面是我將教程裡的資料夾命名和結構稍加修改後的一個方案,可供大家參考:

從一個實戰專案來看一下React Native開發的幾個關鍵技術點

三. 總結

從最開始的FlexBox佈局的學習到現在這個專案的總結完成有了快兩個月的時間了。我在這裡說一下這段學習過程中的一些感受:

關於學習成本

我覺得這一點應該是所有未接觸到React Native的人最關心的一點了,所以我將它放到了總結裡的第一位。我在這裡取兩種典型的群體來做比較:

  1. 只會某種Native開發但是不會JavaScript等前端知識的人群。
  2. 只會前端知識但是不會任何一種Native開發的人群。

對於這兩種人群來說,在React Native的學習過程中成本都不小。但不同的是,這兩種人群的學習成本在整個學習過程中的不同階段是不一樣的。怎麼說呢?

對於第一種人群,因為缺乏前端相關知識,所以在組建的佈局,以及JavaScript的語法上會有點吃力。而這兩點恰恰是React Native學習的敲門磚,因此,對於這種群體,在學習React Native的初期會比較吃力,學習成本很大。

關於如何配合視訊來學習

在結合視訊學習的時候一定要跟上思路,如果講師是邊寫程式碼邊講解,就一定要弄清楚每一行程式碼的意義在哪裡,為什麼要這麼寫,千萬不要怕浪費時間而快速略過。停下腳步來思考實際上是節省時間:因為如果你不試著去理解程式碼和講師的思路,在後來你會越來越看不懂,反而浪費大量時間重新回頭看。

所以我認為最好是先聽一遍講師講的內容,理清思路,然後再動手寫程式碼,這樣效率會比較高,在將來出現的問題也會更少。

四. 學習參考資料

下面是我近1個半月以來收集的比較好的React Native入門資料和部落格,分享給大家:

因為接觸React Native的開發時間還不到2個月,所以有些地方難免理解的不夠透徹或者理解有誤,希望發現問題的同學多多批評或提出寶貴的建議~


本文已經同步到我的個人部落格:從一個實戰專案來看一下React Native開發的幾個關鍵技術點

歡迎來參觀 ^^

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章