React Native 的路由架构分享以及配套神器推荐

历时一个多月的加班加点,我的第一个 React Native 应用终于开始交付测试。这篇文章给大家分享一点 React Native 路由架构的心得,我的技术有限,希望和大家多多交流学习。
开发 React Native 的应用时,第一个要解决的问题是环境搭建,第二个要解决的问题就是路由架构,只有在搭建好路由之后,后面的工作才能依次展开。
搭建路由系统推荐使用 react-navigation 这个官方推荐的组件,该组件有三种导航(路由)系统:

  • 栈导航系统 StackNavigator
  • 标签导航系统 TabNavigator
  • 抽屉导航系统 DrawerNavigator

在我的应用中没有使用到抽屉导航系统,因此这里就不介绍这块相关的内容了(我也没看)。首先来看一下应用的基本结构。
PS.由于我比较懒,这里就不提供截图了,采用文字描述的形式,各位要是有不明白的地方可以问我。

应用基本结构

应用的基本结构如下:

闪屏和登陆

应用运行时,首先进入 Splash 闪屏,一段时间后跳转到登陆界面,登陆之后跳转到主页。在用户登陆时会有一个本地的持久化处理,如果用户登陆成功,那么下一次运行应用时,会直接跳转到主页。

主页

主页整体上是一个标签导航系统,整个标签导航系统分为四个标签:首页、数据、消息和我的。每个标签页中还拥有一些子路由,层次最多为三层,这个就不详细说了。

路由分层

根据前面的应用基本结构,可以将使用 APP 时的路由分为两层:从闪屏到登陆到主页为第一层,主页及其内部的路由为第二层。
分层之后,就可以搭建路由系统了。整体上采用栈式导航(StackNavigator),将闪屏页、登录页和主页作为栈式导航的子路由,主页内部采用标签式导航(TabNavigator)。

目录结构

我们采用如下的目录结构:

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

下面解释下这些目录的作用:

  • components:存放公用组件
  • data:对接后端的 API,针对每个 tab 页面使用一个独立文件
  • images:项目中用到的图片
  • login:登陆界面
  • scene:闪屏(Splash)界面
  • tabs:存放主页中的界面,依据不同的 tab 进行子文件夹划分
  • utils:公共函数和配置等

路由配置

先来配置主页中的各个 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';
// 首页的一个界面
import HomeShowTab from './tabs/home_tab/HomeShowTab';
...
// 数据页的一个界面
import DataShowTab from './tabs/data_tab/DataShowTab';
...
// 消息页的一个界面
import MessageShowTab from './tabs/message_tab/MessageShowTab';
...
// 我的页的一个界面
import MineShowTab from  './tabs/Mine_tab/MineShowTab';
...

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

// 定义数据 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"
    }
);

对于每一个 Tab 来说,它们内部应该使用栈式导航系统。
接下来,定义主页的标签导航:

// 底部菜单栏设置
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:'数据',
                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,
            },
        }
    }
);

主页整体采用标签式导航,将每个标签的 screen 指向前面定义的各个 Tab。
接下来加入闪屏和登陆,构建整体的导航系统:

// 整体路由系统
const RootNavigator = StackNavigator({
    IndexScreen: {
        screen: MainScreenNavigator,
    },
    Splash:{screen: SplashScreen},
    Login:{screen: Login},

}, {
    // 默认显示界面为 Splash
    initialRouteName: "Splash",
    mode: 'card',
    headerMode: 'none',
});

然后导出我们配置的路由系统就可以了:

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

至此,我们的导航系统就搭建好了,这是一个比较通用的系统,基本可以适用于一般的应用了。构建导航系统之后,剩下的工作就是在项目目录中添加各种各样的组件,以及使用 navigate 方法进行页面间的跳转了。
如果你是开发 IOS 应用,这样的架构就已经足够了,但如果你还要同时适配 Android(一般都会),就还需要做一点工作。

Android 的返回键问题

还记得吗?我们的应用是从 Splash 闪屏开始,根据用户是否登陆跳转到登陆界面或者主界面,在 IOS 下是没有问题的,但在 Android 下,由于返回键的存在,当跳转到登陆或者主界面时,还可以按返回键返回到 Splash 界面或者登陆界面,这显然是不合常理的。因此,在 Android 下,需要我们手动的对返回键进行处理。这就需要使用到 BackHandler 组件。
我们需要在两个界面对 BackHandler 组件进行处理:一个是登陆界面(阻止返回到 Splash 界面),另一个是在首页 Tab 的第一个界面(阻止返回到登陆界面)。在这两个界面中,我们需要对 BackHandler 进行事件监听,在用户连续点击两次返回键时退出应用,阻止默认的返回事件。
要完成这个功能,需要用到两个神器:react-navigation-is-focused-hoc 组件和 react-native-exit-app组件。

两个实用的组件

react-navigation-is-focused-hoc 是用来判断某个页面是否处于 Focus 状态。为什么需要这个组件呢?在 Android 上,当我们在某个界面对物理返回键进行事件监听时,会影响到所有界面的物理返回键功能,因此我们需要在跳转到其他界面之前移除对物理返回键的事件监听,在跳转回来时重新绑定事件监听。
跳转到其他页面时移除事件监听还好说,但是怎么对跳转回当前界面进行判断呢?因为有些跳转是通过 navigation.goBack() 进行的,并不会触发组件的生命周期,所以判断是相当麻烦的。react-navigation-is-focused-hoc 这个组件就是帮助我们来解决这个问题的。
PS.后续版本的 react-navigation 组件可能会开发相应的生命周期函数,请参考 #51
react-native-exit-app 这个组件是干嘛的呢?这是因为我们在连续两次点击返回键时需要退出应用,如果使用 BackHandler 自带的 exitApp() 方法,无法完全结束应用的进程(参见#13483),导致下一次进入应用时返回键失效,因此我们需要使用 react-native-exit-app 这个组件实现应用的完全退出。

具体应用

下面是这两个组件的使用方法:
1.对跟路由组件的 onNavigationStateChange 事件进行监听:

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.对要监听物理返回键的界面进行处理:

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

class HomeShowTab extends PureComponent {
    ...
    // 应用更新时绑定/解绑事件
    componentDidUpdate(prevProps) {
        const { isFocused } = this.props;
        if(isFocused){
            this.preventBackEvent();
        }else{
            this.removeBackEvent();
        }
    }      

    preventBackEventHander(){
        const time = +new Date();
        this.refs.toast.show("再按一次退出应用")
        if(!this.exitTimeFlag){
            this.exitTimeFlag = time;
            return true;
        }
        // 2500ms 内连续按键退出应用
        if(time - this.exitTimeFlag < 2500){
            this.removeBackEvent();
            this.timer = setTimeout(()=>{
                clearTimeout(this.timer);
                RNExitApp.exitApp();
            },200);
        }
        this.exitTimeFlag = time;
        
        return true;
    }

    // 绑定事件监听
    preventBackEvent(){
        BackHandler.addEventListener("hardwareBackPress",this.preventBackEventHander)
    }

    componentWillUnmount(){
        this.removeBackEvent();
    }

    // 移除事件监听
    removeBackEvent(){
        BackHandler.removeEventListener("hardwareBackPress",this.preventBackEventHander)
    }
    ...
}

然后,使用 withNavigationFocus 高阶组件进行一次包装即可:

export default withNavigationFocus(HomeShowTab)

可见,react-navigation-is-focused-hoc 的原理是对跟路由组件的 onNavigationStateChange 事件进行监听,当发生路由跳转时,将属性传递到对应的组件,以实现对界面是否处于 Focus 的判断。

安卓返回键问题的其他解决方案

针对安卓返回键的问题,我还看了其余的两个解决方案:

  • 集成 Redux,参见这里
  • 使用 getStateForAction 手动对路由栈进行管理

对于中小型的应用,没有必要使用 Redux,而对于使用 getStateForAction 手动对路由栈进行管理太过麻烦,需要考虑很多情况,我也没有研究透。
因此,对我个人而言,使用 react-navigation-is-focused-hocreact-native-exit-app 这两个组件是比较好的解决方案,这两个组件帮助我解决了安卓返回键这一大痛点,因此我将它们称为神器。

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容