目录
一. 项目导航框架的结构
二. 项目导航框架的实现
react-navigation
的一些基础知识和常用API,本文就不再讲解了,可以去它的官网查阅并学习。那本文着重讲解的是使用react-navigation
搭建项目导航框架的结构及实现。
一. 项目导航框架的结构
我们先回顾一下iOS项目导航框架的结构:UITabBarController
作为根容器,然后每个tabBar item
对应一个UINavigationController
,而每个UINavigationController
都拥有一个导航栈。
对应到RN里:BottomTabNavigator
作为根容器,然后每个tabBar item
对应一个StackNavigator
,而每个StackNavigator
都拥有一个导航栈。
RN里如果真得使用这种项目导航框架的结构,使用习惯虽然跟我们iOS里比较像,但是它使用起来却不是我们iOS里那样。很简单一个例子,如果我们有一个详情界面,四个tabbar
都有可能跳到这个详情界面,那我们就得把详情界面分别添加到每个StackNavigator
的路由里,这代码明显是重复的,我们iOS里可不需要这么做,而且一旦实际开发中类似这样好多个界面都有可能有四个入口,那写起来就会炸掉。
因此不建议在RN里使用类似于iOS的那种导航框架,而是采用类似于安卓的一种导航框架:把StackNavigator
作为根容器,然后把一个BottomTabNavigator
放进StackNavigator
里,但是BottomTabNavigator
的每个tabBar item
需对应一个不带导航栏的界面。这类似于一口井,StackNavigator
就是这口井,井盖就是StackNavigator
的导航栏,而BottomTabNavigator
就像一个排桶架,我们可以往排桶架上面放入多个没有盖的水桶——即界面。这样我们每push
一个界面,其实都是往水井里放一个东西,盖在原来的排桶架上,也就是push
进来的界面和BottomTabNavigator
是同级别的,这其实很违反我们看界面效果直观上的理解,但这种方式在RN和安卓里编写起代码来比较合理。同时我们也可以发现这种结构有一个问题那就是:我们无法设置BottomTabNavigator
上每个页面的导航栏,这需要额外的处理,因为我们看到的永远只能是最外层StackNavigator
的导航栏,也就是说我们只能看到井盖,即便你给水桶盖了盖,别人也看不到。
以上就是项目导航框架的主体,但是实际开发中我们是肯定会为App添加启动页、引导页、广告页等,所以此时我们还需要给项目的主体导航框架外套上一层,即SwitchNavigator
。
而且我们又知道使用react-navigation
,必须得用createAppContainer()
包装一下根组件才能用,因此SwitchNavigator
外面还得再套一层AppContainer
。
于是我们就得到了最终项目导航框架的结构,一共四层,从内到外依次是:BottomTabNavigator
、StackNavigator
、SwitchNavigator
、AppContainer
。我们实际开发中搭建项目导航框架,也是按着这个,从内往外搭就行了:
- 第一步:先搭最内层的
BottomTabNavigator
。 - 第二步:后搭根容器
StackNavigator
,并把BottomTabNavigator
放进根容器StackNavigator
里。 - 第三步:再搭
SwitchNavigator
,并把根容器StackNavigator
放进SwitchNavigator
里。 - 第四步:最后给
SwitchNavigator
套一层AppContainer
,就可以使用了。
二. 项目导航框架的实现
- 添加
react-navigation
相关的组件,并Link原生所有的依赖。
yarn add react-navigation
yarn add react-native-gesture-handler
react-native link react-native-gesture-handler
- 搭建
BottomTabNavigator
。
一些注意的地方:
1、
BottomTabNavigator
类似于我们iOS的UITabBarController
,专门用来负责tabBar下面这一部分和四个模块首页的展示,它是无法配置导航栏的。2、当我们把
BottomTabNavigator
作为别的容器的路由时,它也就有了navigation属性,BottomTabNavigator
的navigation属性不是说不能用来做跳转,肯定能,但是它的跳转效果仅仅是点击tabBar切换页面那种效果,不是我们常见到的那种push或者modal的效果,而且即便它有push或模态的效果,我们也不可能用它来做整个App中界面的跳转,因为我们每往BottomTabNavigator
中添加一个路由,tabBar上就会多一个tab,这根本不是我们想要的效果。3、而且到了后面的开发中,我们不会像下面代码中那样去配置每个页面的导航栏,而是会自定义导航栏,在每个界面中分别添加,这样代码会更低耦合和灵活。
// BottomTabNavigator.js
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Entypo from 'react-native-vector-icons/Entypo';
import {createBottomTabNavigator} from "react-navigation";
import Color from '../Const/Color';
import FavoritePage from "../page/FavoritePage";
import TrendingPage from "../page/TrendingPage";
import PopularPage from "../page/PopularPage";
import MyPage from "../page/MyPage";
import NavigationUtil from "./NavigationUtil";
const BottomTabNavigator = createBottomTabNavigator({
// 路由配置
PopularPage: {
screen: PopularPage,
navigationOptions: {
tabBarLabel: '最热',
tabBarIcon: ({tintColor}) => (
<MaterialIcons
name={'whatshot'}
size={26}
style={{color: tintColor}}
/>
),
},
},
TrendingPage: {
screen: TrendingPage,
navigationOptions: {
tabBarLabel: '趋势',
tabBarIcon: ({tintColor}) => (
<Ionicons
name={'md-trending-up'}
size={26}
style={{color: tintColor}}
/>
),
},
},
FavoritePage: {
screen: FavoritePage,
navigationOptions: {
tabBarLabel: '收藏',
tabBarIcon: ({tintColor}) => (
<MaterialIcons
name={'favorite'}
size={26}
style={{color: tintColor}}
/>
),
},
},
MyPage: {
screen: MyPage,
navigationOptions: {
tabBarLabel: '我的',
tabBarIcon: ({tintColor}) => (
<Entypo
name={'user'}
size={26}
style={{color: tintColor}}
/>
),
},
},
}, {
tabBarOptions: {
// 选中颜色
activeTintColor: Color.THEME_COLOR,
// 未选中颜色
inactiveTintColor: Color.INACTIVE_TINT_COLOR,
}
});
// 这个方法会走的前提是BottomTabNavigator被放在了另一个容器视图里作为路由,否则它是没有navigation的
BottomTabNavigator.navigationOptions = ({navigation}) => {
const {routeName} = navigation.state.routes[navigation.state.index];
switch (routeName) {
// case 'PopularPage': return {header: (// 如果某个页面的导航栏就是个TopNavigator,也可以在这里配置没问题,
// // 但是因为TopNavigator可能要操作很多界面,都配置在这里让这个文件显得有点累赘,所以我们就去相应的界面里配置它了,而不在这里配置
// <TopNavigator/>
// )};
// break;
case 'PopularPage': return {header: null};
break;
case 'TrendingPage': return {headerTitle: '趋势'};
break;
case 'FavoritePage': return {headerTitle: '收藏'};
break;
case 'MyPage': return {headerTitle: '我的'};
break;
}
};
export default BottomTabNavigator;
- 搭建
StackNavigator
一些注意的地方:
前面提到了
BottomTabNavigator
是专门用来做底部的tabbar和四个模块首页界面的展示的,而且也提到了它是无法配置导航栏的和我们不可能用它的navigation
做界面跳转(不是不能,是用的效果和我们预期不一样),这就引出了StackNavigator
,这是我们非得用它不可的理由。
StackNavigator
专门用来配置每个界面的导航栏和做界面的跳转,App中所有界面的跳转都必须用StackNavigator
的navigation
,但同样StackNavigator
也必须得作为别人的路由存在时,它才有navigation
属性,否则没有。上面一段我们说了一句话“App中所有界面的跳转都必须用
StackNavigator
的navigation
属性”,其实这句话说的有点绝对了,其实每个界面的navigation
属性都可以用来做跳转,只不过有的情况下会出现问题,如果学的不好,我们找bug很难找,比如你现在可以打开PopularPage.js
界面,看看里面PopularTabPage
组件里打得注释就知道有可能出现的问题了,所以我们还是建议整个App中全部使用StackNavigator
的navigation
属性做跳转,反正它是App的根容器嘛,所有的界面都在它里面,它们之间是可以随便跳转的,可以省去很多麻烦,就像我们iOS里在同一个导航栈下的所有界面其实都是用self.navigationController
来做跳转的,而所有界面的self.navigationController
其实都是同一个,都是栈底父容器的那个navigationController
。此时你也可以想一下,DeatilPage
其实和BottomTabNavigator
的是同级的,但DeatilPage
和PopularPage
、TrendingPage
、FavoritePage
、MyPage
不是同级的啊,但它们之间还是可以跳转,所以这表明只要界面和界面之间在一个大容器里就可以跳转。
// StackNavigator.js
import {createStackNavigator} from "react-navigation";
import NavigationUtil from "./NavigationUtil";
import Color from '../Const/Color';
import BottomTabNavigator from './BottomTabNavigator';
import DynamicBottomTabNavigator from './DynamicBottomTabNavigator';
import DetailPage from "../page/DetailPage";
const StackNavigator = createStackNavigator({
// 路由配置
// BottomTabNavigator: BottomTabNavigator,
DynamicBottomTabNavigator: DynamicBottomTabNavigator,
DetailPage: {
screen: DetailPage,
navigationOptions: {
headerTitle: '详情',
}
},
}, {
defaultNavigationOptions: ({navigation}) => {
// 注意:通过navigationOptions或defaultNavigationOptions的{navigation}获取到的navigation都是它内部包含的路由的navigation属性
// 而且它内部有几个子路由,这个箭头函数就会走几次,全部获取给你获取到
// 因此,StackNavigator的navigation属性其实应该在它所在的容器里获取,即SwitchNavigator
return {
headerStyle: {
backgroundColor: Color.THEME_COLOR,
},
headerTitleStyle: {
color: 'white',
},
headerBackTitle: '返回',
headerBackTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
}
},
mode: 'modal',
});
export default StackNavigator;
- 搭建
SwitchNavigator
一些注意的地方:
如果我们要做启动页、引导页、广告页这种只展示一次,就跳转到其它页面的效果,常规情况下还是会想到用
navigate
方法来跳转,但是goBack
方法却无法想安卓的finish
方法一样把启动页、引导页、广告页从栈里面清除掉,因此iOS侧滑或者安卓的虚拟返回按键还能返回到启动页、引导页、广告页,这就不对了。因此RN提供了
SwitchNavigator
,它的用途就是一次只显示一个页面,跳转后清理掉栈内跳转之前的界面,类似于我们iOS里的切换window的rootViewController——即切换项目根容器的效果。
// SwitchNavigator.js
import {createSwitchNavigator} from "react-navigation";
import WelcomePage from "../page/WelcomePage";
import StackNavigator from './StackNavigator';
import Color from "../Const/Color";
import NavigationUtil from "./NavigationUtil";
const SwitchNavigator = createSwitchNavigator({
// 路由配置
WelcomePage: WelcomePage,// 启动页、引导页、广告页
StackNavigator: StackNavigator,
}, {
defaultNavigationOptions: ({navigation}) => {
// NavigationUtil的一个静态变量,记录根容器StackNavigator的navigation,用来做整个App内部的跳转
if (navigation.state.routeName === 'StackNavigator') {
NavigationUtil.navigation = navigation;
}
},
});
export default SwitchNavigator;
- 搭建AppContainer
-----------AppContainer.js-----------
import {createAppContainer} from "react-navigation";
import SwitchNavigator from './SwitchNavigator';
const AppContainer = createAppContainer(SwitchNavigator);
export default AppContainer;
-
index.js
和App.js
文件、使用AppContainer
我们每创建一个RN项目,系统都默认给我们创建了两个文件,
index.js
和App.js
。
index.js
类似于我们iOS里的main.m
。iOS里main.m
是整个程序的入口,里面将AppDelegate
作为了整个程序的代理,RN里index.js
是整个程序的入口,里面将App
作为了整个程序的代理。
App.js
类似于我们iOS里的AppDelegate.m
。iOS里我们在AppDelegate.m
里设置window
的rootViewController
,RN里我们在App.js
里设置整个项目的根组件,即App.js
文件导出的组件,就是整个项目的根组件,它位于所有组件的最下方。
所以一般情况下,我们不变动index.js
文件,它代码固定为:
-----------index.js-----------
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
而是在App.js
里设置整个项目的根组件:
-----------App.js-----------
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
// 导入项目的根组件
import AppContainer from './js/navigator/AppContainer';
export default class App extends Component<Props> {
render() {
return (
// 设置项目的根组件
<AppContainer/>
);
}
}
- 编写项目跳转工具类
// NavigationUtil.js
/**
* 我们专门写一个负责跳转的工具类,方便项目中跳转的统一管理
*/
export default class NavigationUtil {
// 一个静态变量,记录根容器StackNavigator的navigation,因为项目的根容器是一个StackNavigator嘛,所以项目中的跳转都是用它的navigation
static navigation;
/**
* 跳转到上一页
*/
static goBack() {
// 根navigation无法goBack,但是它可以pop
this.navigation.pop();
}
/**
* 跳转到指定页面
*
* @param page 要传递的参数
* @param params 要跳转的页面路由名
*/
static navigate(page, params) {
// 但是请注意:
// App中所有界面的跳转都是通过这个方法来跳转的,包括启动页、引导页、广告页跳转到StackNavigator,那就要想到这个时候也用StackNavigator的navigation属性做跳转能成功吗?
// 答案是:能成功!
// 你可以回想一下,我们只要在同一个导航栈中的界面,其实不一定非要拿栈底那个根容器的navigation属性来做跳转,其实拿其中任意一个界面的navigation属性做跳转都可以
// 此处也是同理的,因为SwitchNavigator没有作为别人的路由存在,所以SwitchNavigator没有navigation属性,我们就只能那栈内界面的navigation属性做跳转了,那WelcomePage或者StackNavigator的navigation都行,但为了项目的统一性,我们就拿StackNavigator的了
if (!this.navigation) {
console.log('NavigationUtil.navigation不能为空!');
return;
}
this.navigation.navigate(page, params);
}
}