搭建路由系統(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-hoc 和 react-native-exit-app 這兩個(gè)組件是比較好的解決方案,這兩個(gè)組件幫助我解決了安卓返回鍵這一大痛點(diǎn),因此我將它們稱為神器。
作者:黑黢黢
鏈接:http://m.itdecent.cn/p/87ad53cefd06
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。