目錄
一. 項目導航框架的結構
二. 項目導航框架的實現
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);
}
}