[轉(zhuǎn)]React Native 的路由架構(gòu)分享以及配套神器推薦

搭建路由系統(tǒng)推薦使用 react-navigation 這個(gè)官方推薦的組件,該組件有三種導(dǎo)航(路由)系統(tǒng):

  • 棧導(dǎo)航系統(tǒng) StackNavigator
  • 標(biāo)簽導(dǎo)航系統(tǒng) TabNavigator
  • 抽屜導(dǎo)航系統(tǒng) DrawerNavigator

在我的應(yīng)用中沒有使用到抽屜導(dǎo)航系統(tǒng),因此這里就不介紹這塊相關(guān)的內(nèi)容了(我也沒看)。首先來看一下應(yīng)用的基本結(jié)構(gòu)。
PS.由于我比較懶,這里就不提供截圖了,采用文字描述的形式,各位要是有不明白的地方可以問我。

應(yīng)用基本結(jié)構(gòu)

應(yīng)用的基本結(jié)構(gòu)如下:

閃屏和登陸

應(yīng)用運(yùn)行時(shí),首先進(jìn)入 Splash 閃屏,一段時(shí)間后跳轉(zhuǎn)到登陸界面,登陸之后跳轉(zhuǎn)到主頁。在用戶登陸時(shí)會有一個(gè)本地的持久化處理,如果用戶登陸成功,那么下一次運(yùn)行應(yīng)用時(shí),會直接跳轉(zhuǎn)到主頁。

主頁

主頁整體上是一個(gè)標(biāo)簽導(dǎo)航系統(tǒng),整個(gè)標(biāo)簽導(dǎo)航系統(tǒng)分為四個(gè)標(biāo)簽:首頁、數(shù)據(jù)、消息和我的。每個(gè)標(biāo)簽頁中還擁有一些子路由,層次最多為三層,這個(gè)就不詳細(xì)說了。

路由分層

根據(jù)前面的應(yīng)用基本結(jié)構(gòu),可以將使用 APP 時(shí)的路由分為兩層:從閃屏到登陸到主頁為第一層,主頁及其內(nèi)部的路由為第二層。
分層之后,就可以搭建路由系統(tǒng)了。整體上采用棧式導(dǎo)航(StackNavigator),將閃屏頁、登錄頁和主頁作為棧式導(dǎo)航的子路由,主頁內(nèi)部采用標(biāo)簽式導(dǎo)航(TabNavigator)。

目錄結(jié)構(gòu)

我們采用如下的目錄結(jié)構(gòu):

├─components
├─data
├─images
├─login
├─scene
├─tabs
│  ├─data_tab
│  │  └─dataComponents
│  ├─home_tab
│  ├─message_tab
│  └─Mine_tab
└─utils

下面解釋下這些目錄的作用:

  • components:存放公用組件
  • data:對接后端的 API,針對每個(gè) tab 頁面使用一個(gè)獨(dú)立文件
  • images:項(xiàng)目中用到的圖片
  • login:登陸界面
  • scene:閃屏(Splash)界面
  • tabs:存放主頁中的界面,依據(jù)不同的 tab 進(jìn)行子文件夾劃分
  • utils:公共函數(shù)和配置等

路由配置

先來配置主頁中的各個(gè) Tab:

// 引入路由組件
import {
    StackNavigator,
    TabNavigator,
} from 'react-navigation'

import {
    Dimensions,
    ...
} from 'react-native'

// 獲取屏幕寬度
const { width } = Dimensions.get('window');
// 閃屏界面
import SplashScreen from './scene/Splash'
// 登陸界面
import Login from './login/Login';
// 首頁的一個(gè)界面
import HomeShowTab from './tabs/home_tab/HomeShowTab';
...
// 數(shù)據(jù)頁的一個(gè)界面
import DataShowTab from './tabs/data_tab/DataShowTab';
...
// 消息頁的一個(gè)界面
import MessageShowTab from './tabs/message_tab/MessageShowTab';
...
// 我的頁的一個(gè)界面
import MineShowTab from  './tabs/Mine_tab/MineShowTab';
...

// 定義首頁 Tab
const HomeTab=StackNavigator(
    {
        HomeShowTab: {
            screen: HomeShowTab,
        },
        ...
    },
    {
        headerMode: "screen"
    }
);

// 定義數(shù)據(jù) Tab
const DataTab=StackNavigator(
    {
        DataShowTab: {
            screen: DataShowTab,
        },
        ...
    },
    {
        headerMode: "screen"
    }
);

// 定義消息 Tab
const MessageTab=StackNavigator(
    {
        FirstScreen: {
            screen: MessageShowTab,
            navigationOptions: {title: "消息"},
        },
        ...
    },
    {
        headerMode: "screen"
    }
);

// 定義我的 Tab
const MineTab=StackNavigator(
    {
        MineShowTab: {
            screen: MineShowTab,
        },
        ...
    },
    {
        headerMode: "screen"
    }
);

對于每一個(gè) Tab 來說,它們內(nèi)部應(yīng)該使用棧式導(dǎo)航系統(tǒng)。
接下來,定義主頁的標(biāo)簽導(dǎo)航:

// 底部菜單欄設(shè)置
const MainScreenNavigator = TabNavigator({
        HomeScreen: {
            screen: HomeTab,
            navigationOptions: {
                tabBarLabel: '首頁',
                tabBarIcon: ({ tintColor,focused }) => {
                    return(
                        !focused?
                            <Image
                                source={require('./images/tab_home_normal.png')}
                                style={[styles.icon]}
                            />
                        :
                            <Image
                                source={require('./images/tab_home_pre.png')}
                                style={[styles.icon]}
                            />
                    );
                },
            },
        },
        DataScreen: {
            screen: DataTab,
            navigationOptions: {
                tabBarLabel:'數(shù)據(jù)',
                tabBarIcon: ({ tintColor,focused }) => {
                    return(
                        !focused?
                            <Image
                                source={require('./images/tab_data_normal.png')}
                                style={[styles.icon]}
                            />
                        :
                            <Image
                                source={require('./images/tab_data_pre.png')}
                                style={[styles.icon]}
                            />
                    );
                },
            }
        },
        MessageScreen: {
            screen: MessageTab,
            navigationOptions: {
                tabBarLabel:'消息',
                tabBarIcon: ({ tintColor,focused }) => {
                    return(
                        !focused?
                            <Image
                                source={require('./images/tab_word_normal.png')}
                                style={[styles.icon]}
                            />
                        :
                            <Image
                                source={require('./images/tab_word_pre.png')}
                                style={[styles.icon]}
                            />
                    );
                },
            }
        },
        MineScreen: {
            screen: MineTab,
            navigationOptions: {
                tabBarLabel:'我的',
                tabBarIcon: ({ tintColor,focused }) => {
                    return(
                        !focused?
                            <Image
                                source={require('./images/tab_center_normal.png')}
                                style={[styles.icon]}
                            />
                        :
                            <Image
                                source={require('./images/tab_center_pre.png')}
                                style={[styles.icon]}
                            />
                    );
                },
            }
        }
    },
    {
        initialRouteName:'HomeScreen',
        lazy:true,
        animationEnabled: false,
        tabBarPosition: 'bottom',
        swipeEnabled: false,
        tabBarOptions: {
            activeTintColor: '#42aff4',
            inactiveTintColor: '#999',
            showIcon: true,
            indicatorStyle: {
                height: 0
            },
            style: {
                backgroundColor: '#f0f3f5',
                height: 0.13066667 * width,
                justifyContent:"center",
            },
            labelStyle: {
                fontSize: 0.0293333 * width,
                marginTop:-0.008 * width,
            },
        }
    }
);

主頁整體采用標(biāo)簽式導(dǎo)航,將每個(gè)標(biāo)簽的 screen 指向前面定義的各個(gè) Tab。
接下來加入閃屏和登陸,構(gòu)建整體的導(dǎo)航系統(tǒng):

// 整體路由系統(tǒng)
const RootNavigator = StackNavigator({
    IndexScreen: {
        screen: MainScreenNavigator,
    },
    Splash:{screen: SplashScreen},
    Login:{screen: Login},

}, {
    // 默認(rèn)顯示界面為 Splash
    initialRouteName: "Splash",
    mode: 'card',
    headerMode: 'none',
});

然后導(dǎo)出我們配置的路由系統(tǒng)就可以了:

export default class MyAPP extends Component {
    render() {
        return (
            <View style={styles.container}>
                <RootNavigator />
            </View>
        )
    }
}

至此,我們的導(dǎo)航系統(tǒng)就搭建好了,這是一個(gè)比較通用的系統(tǒng),基本可以適用于一般的應(yīng)用了。構(gòu)建導(dǎo)航系統(tǒng)之后,剩下的工作就是在項(xiàng)目目錄中添加各種各樣的組件,以及使用 navigate 方法進(jìn)行頁面間的跳轉(zhuǎn)了。
如果你是開發(fā) IOS 應(yīng)用,這樣的架構(gòu)就已經(jīng)足夠了,但如果你還要同時(shí)適配 Android(一般都會),就還需要做一點(diǎn)工作。

Android 的返回鍵問題

還記得嗎?我們的應(yīng)用是從 Splash 閃屏開始,根據(jù)用戶是否登陸跳轉(zhuǎn)到登陸界面或者主界面,在 IOS 下是沒有問題的,但在 Android 下,由于返回鍵的存在,當(dāng)跳轉(zhuǎn)到登陸或者主界面時(shí),還可以按返回鍵返回到 Splash 界面或者登陸界面,這顯然是不合常理的。因此,在 Android 下,需要我們手動(dòng)的對返回鍵進(jìn)行處理。這就需要使用到 BackHandler 組件。
我們需要在兩個(gè)界面對 BackHandler 組件進(jìn)行處理:一個(gè)是登陸界面(阻止返回到 Splash 界面),另一個(gè)是在首頁 Tab 的第一個(gè)界面(阻止返回到登陸界面)。在這兩個(gè)界面中,我們需要對 BackHandler 進(jìn)行事件監(jiān)聽,在用戶連續(xù)點(diǎn)擊兩次返回鍵時(shí)退出應(yīng)用,阻止默認(rèn)的返回事件。
要完成這個(gè)功能,需要用到兩個(gè)神器:react-navigation-is-focused-hoc 組件和 react-native-exit-app組件。

兩個(gè)實(shí)用的組件

react-navigation-is-focused-hoc 是用來判斷某個(gè)頁面是否處于 Focus 狀態(tài)。為什么需要這個(gè)組件呢?在 Android 上,當(dāng)我們在某個(gè)界面對物理返回鍵進(jìn)行事件監(jiān)聽時(shí),會影響到所有界面的物理返回鍵功能,因此我們需要在跳轉(zhuǎn)到其他界面之前移除對物理返回鍵的事件監(jiān)聽,在跳轉(zhuǎn)回來時(shí)重新綁定事件監(jiān)聽。
跳轉(zhuǎn)到其他頁面時(shí)移除事件監(jiān)聽還好說,但是怎么對跳轉(zhuǎn)回當(dāng)前界面進(jìn)行判斷呢?因?yàn)橛行┨D(zhuǎn)是通過 navigation.goBack() 進(jìn)行的,并不會觸發(fā)組件的生命周期,所以判斷是相當(dāng)麻煩的。react-navigation-is-focused-hoc 這個(gè)組件就是幫助我們來解決這個(gè)問題的。
PS.后續(xù)版本的 react-navigation 組件可能會開發(fā)相應(yīng)的生命周期函數(shù),請參考 #51。
react-native-exit-app 這個(gè)組件是干嘛的呢?這是因?yàn)槲覀冊谶B續(xù)兩次點(diǎn)擊返回鍵時(shí)需要退出應(yīng)用,如果使用 BackHandler 自帶的 exitApp() 方法,無法完全結(jié)束應(yīng)用的進(jìn)程(參見#13483),導(dǎo)致下一次進(jìn)入應(yīng)用時(shí)返回鍵失效,因此我們需要使用 react-native-exit-app 這個(gè)組件實(shí)現(xiàn)應(yīng)用的完全退出。

具體應(yīng)用

下面是這兩個(gè)組件的使用方法:
1.對跟路由組件的 onNavigationStateChange 事件進(jìn)行監(jiān)聽:

import { updateFocus } from '@patwoz/react-navigation-is-focused-hoc'
...
export default class MyAPP extends Component {
    render() {
        return (
            <View style={styles.container}>
                <RootNavigator
                    onNavigationStateChange={(prevState, currentState) => {
                        updateFocus(currentState)
                    }}
                />
            </View>
        )
    }
}

2.對要監(jiān)聽物理返回鍵的界面進(jìn)行處理:

import { withNavigationFocus } from '@patwoz/react-navigation-is-focused-hoc'
import RNExitApp from 'react-native-exit-app';

class HomeShowTab extends PureComponent {
    ...
    // 應(yīng)用更新時(shí)綁定/解綁事件
    componentDidUpdate(prevProps) {
        const { isFocused } = this.props;
        if(isFocused){
            this.preventBackEvent();
        }else{
            this.removeBackEvent();
        }
    }      

    preventBackEventHander(){
        const time = +new Date();
        this.refs.toast.show("再按一次退出應(yīng)用")
        if(!this.exitTimeFlag){
            this.exitTimeFlag = time;
            return true;
        }
        // 2500ms 內(nèi)連續(xù)按鍵退出應(yīng)用
        if(time - this.exitTimeFlag < 2500){
            this.removeBackEvent();
            this.timer = setTimeout(()=>{
                clearTimeout(this.timer);
                RNExitApp.exitApp();
            },200);
        }
        this.exitTimeFlag = time;

        return true;
    }

    // 綁定事件監(jiān)聽
    preventBackEvent(){
        BackHandler.addEventListener("hardwareBackPress",this.preventBackEventHander)
    }

    componentWillUnmount(){
        this.removeBackEvent();
    }

    // 移除事件監(jiān)聽
    removeBackEvent(){
        BackHandler.removeEventListener("hardwareBackPress",this.preventBackEventHander)
    }
    ...
}

然后,使用 withNavigationFocus 高階組件進(jìn)行一次包裝即可:

export default withNavigationFocus(HomeShowTab)

可見,react-navigation-is-focused-hoc 的原理是對跟路由組件的 onNavigationStateChange 事件進(jìn)行監(jiān)聽,當(dāng)發(fā)生路由跳轉(zhuǎn)時(shí),將屬性傳遞到對應(yīng)的組件,以實(shí)現(xiàn)對界面是否處于 Focus 的判斷。

安卓返回鍵問題的其他解決方案

針對安卓返回鍵的問題,我還看了其余的兩個(gè)解決方案:

  • 集成 Redux,參見這里
  • 使用 getStateForAction 手動(dòng)對路由棧進(jìn)行管理

對于中小型的應(yīng)用,沒有必要使用 Redux,而對于使用 getStateForAction 手動(dòng)對路由棧進(jìn)行管理太過麻煩,需要考慮很多情況,我也沒有研究透。
因此,對我個(gè)人而言,使用 react-navigation-is-focused-hocreact-native-exit-app 這兩個(gè)組件是比較好的解決方案,這兩個(gè)組件幫助我解決了安卓返回鍵這一大痛點(diǎn),因此我將它們稱為神器。

作者:黑黢黢
鏈接:http://m.itdecent.cn/p/87ad53cefd06
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容