為了閱讀體驗,分為上下兩篇。算不上教程,主要目的還是摘取使用這個庫時的常用知識點和解決方案,便于自己記憶和查閱。
本篇梳理React Navigation 5.x 的一些基礎API、嵌套導航注意事項、如何設計合理的嵌套路由等。
第二篇主要講如何實現(xiàn)我自身App的兩個需求:1、在嵌套路由中動態(tài)配置頂部標題欄(tab導航器嵌套在根部stack導航器的首屏);2、監(jiān)聽tab點擊事件,觸發(fā)時將對應screen重置到初始狀態(tài)(數(shù)據(jù))。會把自己的代碼結構放出來。React Navigation 5.x(二)嵌套路由動態(tài)配置標題欄及自定義tabbar點擊事件
循序漸進,我們先看一些干貨,最后再來實現(xiàn)這兩個功能。
一、堆棧導航器Navigator和Screen的具體參數(shù)配置及說明
這邊只列舉比較實用的。如有需要可以去官網(wǎng)查閱API:createStackNavigator
1. 導航器組件Navigator常用參數(shù)(Props):
參數(shù)名 | 說明 |
---|---|
initialRouteName | 導航器初次加載時要渲染的路由的名字,對應Screen(屏幕組件)的name ,默認渲染該棧內第一個Sreen |
screenOptions | 為該棧內所有Sreen配置通用屬性,即可把Screen的options里的屬性提到這里統(tǒng)一設置。當這些Screen有相同的屬性時,沒有必要復制多份并在它們的options上重復設置。相同的屬性,Screen的options里配置的優(yōu)先級更高。 |
keyboardHandlingEnabled | 默認值為true ,若設置為false ,屏幕上的鍵盤將不會在導航到新屏幕時自動消失。 |
headerMode | 設置該棧標題欄的形式 'float' '(ios模式)|'screen' (Android模式)|'none' (不顯示頭部標題欄) |
2. 導航器內的屏幕組件Screen常用參數(shù)(統(tǒng)一在options里配置):
參數(shù)名 | 類型 | 說明 |
---|---|---|
title | String |
Screen標題欄的文字 |
header | Function |
返回一個React Element作為自定義標題欄。要使用這個配置,必須先確保設置Navigator的headerMode:'screen' ,以及定義好標題欄高度,e.g. headerStyle: { height: 80 } 參考示例(1)
|
headerTitle | String | Function |
如果是函數(shù),返回一個接收參數(shù)的React Element,作為自定義標題欄文字組件。 參考示例(2) |
headerShown | Boolean |
屏幕的標題欄顯示與否,在父Navigator沒有設置headerMode: 'none' 的情況下,默認是true
|
headerTitleAlign | Boolean |
屏幕標題欄文字的對齊方式,可選值:left |center ,未設置時,iOS默認居中,Android則靠左。 |
headerRight | Function |
返回一個React Element以自定義標題欄的右側。 |
headerLeft | Function |
返回一個React Element以自定義標題欄的左側。默認使用HeaderBackButton組件 ,你可以使用它來覆蓋后退按鈕,參考示例(3): |
headerStyle | Object |
標題欄樣式,如背景顏色等 |
headerTitleStyle | Object |
標題欄文字組件(headerTitle)樣式 |
headerTintColor | String |
標題欄文字顏色 |
(1) header配置示例
header: ({ scene, previous, navigation }) => {
const { options } = scene.descriptor;
const title =
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: scene.route.name;
return (
<MyHeader
title={title}
leftButton={
previous ? <MyBackButton onPress={navigation.goBack} /> : undefined
}
style={options.headerStyle}
/>
);
};
(2) headerTitle配置示例
function LogoTitle(props) {
return (
<>
<Image
style={{ width: 50, height: 50 }}
source={require('@expo/snack-static/react-native-logo.png')}
/>
<Text>{props.props.title}</Text>
</>
);
}
function StackScreen() {
const title = '自定義標題' ;
// 這邊標題一般都是我們通過獲取路由參數(shù)再經(jīng)過方法判斷確定的,這里寫死為了方便演示如何把title額外傳給自定義組件。
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerTitle: props => <LogoTitle props={{...props, title}} /> }}
/>
</Stack.Navigator>
);
}
(3) headerLeft示例
import { HeaderBackButton } from '@react-navigation/stack';
<Screen
name="Home"
component={HomeScreen}
options={{
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
// Do something
}}
/>
),
}}
/>;
二、navigation
屬性及方法(Actions
)
App里的每個screen組件都能通過props
接收到一個navigation
屬性,它包含了各種調度導航動作的便利功能/方法。不同的導航器接收到的navigation
能執(zhí)行的方法是有區(qū)別的:
這邊只展開講了部分方法,每個藍色Action
都加了API直達鏈接。
比如我們有個導航屏幕:
<Stack.Screen key="Profile-1" name="Profile" component={component} />
-
通用方法:
-
navigate - 導航到特定路由,參數(shù)必須包含name或key屬性,可選參數(shù)params(合并到目標路由的參數(shù))。通俗理解就是轉到這個name/key對應的路由。如果正在當前路由屏幕組件頁面,那么是不會有任何反應的。
格式:navigation.navigate(name: string, [params: object])
navigation.navigate( 'Profile', { user: 'jane' })
-
goBack - 返回導航堆棧歷史記錄的上一個屏幕。
navigation.goBack()
-
reset - 可以讓我們用新的導航狀態(tài)替換當前的導航狀態(tài),即移除現(xiàn)有的已經(jīng)入棧的屏幕和歷史記錄,設置新的入棧屏幕們。如果希望在更改狀態(tài)時保留現(xiàn)有的屏幕,可以使用
CommonActions.set
結合navigation.dispatch
。像這樣:
import { CommonActions } from '@react-navigation/native'; navigation.dispatch( CommonActions.reset({ index: 1, routes: [ { name: 'Home' }, { name: 'Profile', params: { user: 'jane' }, }, ], }) );
-
setParams - 更新當前或特定路由的參數(shù)。 作用就像React的setState,傳入的參數(shù)和舊的params合并對象而非覆蓋。
如果要為特定路由更新參數(shù),則可添加source
屬性,值為該路由的key。
import { CommonActions } from '@react-navigation/native'; navigation.dispatch({ ...CommonActions.setParams({ user: 'Wojtek' }), source: route.key, });
- setOptions 可以在屏幕組件內部,根據(jù)它的props、state、context,來定制我們的屏幕組件選項(screen options),比如title等。
-
navigate - 導航到特定路由,參數(shù)必須包含name或key屬性,可選參數(shù)params(合并到目標路由的參數(shù))。通俗理解就是轉到這個name/key對應的路由。如果正在當前路由屏幕組件頁面,那么是不會有任何反應的。
-
堆棧導航器:
- replace - 用新路由替換導航狀態(tài)navigation state中當前或指定的路由。以下是替換state中特定路由的示例:
import { StackActions } from '@react-navigation/native'; navigation.dispatch({ ...StackActions.replace('Profile', { // 要新替換上的路由name user: 'jane', }), source: route.key, // 要被替換的路由的key target: navigation.dangerouslyGetState().key, // 要新替換上的路由的key });
-
push - 添加一條新路由到導航堆棧頂部。格式同
navigate
和調用navigate
的區(qū)別是,navigate
會先嘗試查找具有目標name的現(xiàn)有路由并跳轉過去,并且僅在堆棧中還沒有這個路由時才推送新路由。而push
在當目標路由已經(jīng)存在于導航堆棧時,仍然會推送新路由,因此一個路由可以多次訪問(形成多條歷史記錄)。 -
pop - 默認回到導航棧歷史記錄的上一步。有一個可選參數(shù)(count),允許你指定彈出多少個屏幕。
navigation.pop(count: number)
-
popToTop - 返回堆棧中的第一個屏幕,關閉所有其他屏幕。
navigation.popToTop()
-
tab選項卡導航器:
-
jumpTo - 跳轉至tab導航器中的現(xiàn)有路由。
格式:navigation.jumpTo(name: string, [params: object])
-
jumpTo - 跳轉至tab導航器中的現(xiàn)有路由。
-
drawer抽屜導航器:
-
jumpTo - 跳轉至drawer導航器中的現(xiàn)有路由。格式同tab導航器的
jumpTo
-
openDrawer - 打開drawer導航器面板。格式:
navigation.openDrawer()
-
closeDrawer - 關閉drawer面板。格式:
navigation.closeDrawer()
-
toggleDrawer - 切換drawer面板開關狀態(tài)。格式:
navigation.toggleDrawer()
-
jumpTo - 跳轉至drawer導航器中的現(xiàn)有路由。格式同tab導航器的
-
高級API參考
-
dispatch - dispatch方法允許我們發(fā)送一個導航動作對象(包含用于生成特定基于某類型導航器的操作方法),來確定導航狀態(tài)如何更新。除非實在無法直接通過navigate,goBack等方法完成我們所需的操作。不然應該避免使用它。盡量都通過
navigation.[普通方法]
屬性來導航。
dispatch可調度的對象除了CommonActions,還有StackActions 、還有DrawerActions 、還有TabActions 。這仨都擴展于CommonActions。
-
dispatch - dispatch方法允許我們發(fā)送一個導航動作對象(包含用于生成特定基于某類型導航器的操作方法),來確定導航狀態(tài)如何更新。除非實在無法直接通過navigate,goBack等方法完成我們所需的操作。不然應該避免使用它。盡量都通過
// 要先獲取特定的導航動作創(chuàng)造器
import { CommonActions } from '@react-navigation/native';
navigation.dispatch(
// 再去觸發(fā)方法
CommonActions.navigate({
name: 'Profile',
params: {
user: 'jane',
},
})
);
三、導航狀態(tài)Navigation state
navigation state是React Navigation存儲應用程序的路由結構和歷史記錄的對象。
比如,在主屏幕嵌套了一個標簽導航器的堆棧導航器,可能具有如下導航狀態(tài):
const state = {
type: 'stack',
key: 'stack-1',
routeNames: ['Home', 'Profile', 'Settings'],
routes: [
{
key: 'home-1',
name: 'Home',
state: {
key: 'tab-1',
routeNames: ['Feed', 'Library', 'Favorites'],
routes: [
{ key: 'feed-1', name: 'Feed', params: { sortBy: 'latest' } },
{ key: 'library-1', name: 'Library' },
{ key: 'favorites-1', name: 'Favorites' },
],
index: 0,
},
},
{ key: 'settings-1', name: 'Settings' },
],
index: 1,
};
每個導航狀態(tài)對象中包含的屬性:
-
type
-這個導航狀態(tài)歸屬的導航器的類型,例如stack
,tab
,drawer
。 -
key
-識別導航器的唯一鍵。 -
routeName
-包含所屬導航器的每個屏幕name
(字符串)的數(shù)組。 -
routes
-在導航器中呈現(xiàn)的路由對象(屏幕)的列表。它還在堆棧導航器中表示歷史記錄。此數(shù)組中至少應存在一項。 -
index
-正獲得焦點的路由對象在routes
數(shù)組中的索引。 -
history
-訪問過的項目列表。這是一個可選屬性,并非在所有導航器中都存在。比如它僅存在于核心的tab和抽屜導航器中。history
數(shù)組中的項目可以根據(jù)導航器而變化。此數(shù)組中至少應存在一項。 -
stale
-除非顯式設置了stale
屬性,否則值默認是false
。也就表示導航狀態(tài)對象需要“自動補齊”。
routes
數(shù)組中的每個路由對象(route
①)都可以包含以下屬性:
-
key
-屏幕的唯一鍵。會自動創(chuàng)建或在導航到此屏幕時添加。 -
name
-屏幕名稱。在導航器組件層次結構中定義。 -
params
-可選,一個包含參數(shù)的對象,有導航動作時定義,例如navigate('Home', { sortBy: 'latest' })
。 -
state
-可選,嵌套在此屏幕內的子導航器的導航狀態(tài)對象。只會在導航事件發(fā)生后才掛到路由對象上。
①注:每個screen組件的props.route
,就是上面說的routes
數(shù)組中的路由對象,內容為這個屏幕的路由數(shù)據(jù)。
四、設計合理的導航結構
嵌套導航器就是在一個Navigator的一個Screen里渲染的Navigator,作為一個組件元素賦給Screen的component屬性。
一個應用通常都擁有底部選項卡(tabbar
),一般是主頁的標準配置。同時應用中的部分頁面(比如登錄頁等)不需要tabbar。要實現(xiàn)這一點,能從導航結構入手就不要去動態(tài)設置隱藏/顯示tabbar。最簡單的方法是將選項卡導航器嵌套在堆棧導航器的第一個屏幕中,將不需要tabbar的Screen放在這個屏幕后面。
像下面的例子:一個tab導航器就被嵌套在stack導航器里。
首頁為底部tab欄的典型嵌套結構 (下面五、(3)還會用這個例子舉證)
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Feed" component={Feed} />
</Tab.Navigator>
);
}
function App() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeTabs} />
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
}
另外一種應用中常見的導航模式,把stack導航器嵌套在drawer導航器的每個屏幕中
function Root() {
return (
<Stack.Navigator>
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Drawer.Navigator>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Root" component={Root} />
</Drawer.Navigator>
</NavigationContainer>
);
}
如果我們想從Home導航到Root,這樣操作:navigation.navigate('Root');
Root的初始屏幕即Profile就會展示。如果你想展示的是Settings這一屏,就需要這樣做:navigation.navigate('Root', { screen: 'Settings' });
如果你要帶參數(shù)跳轉,現(xiàn)在就得這樣做:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
五、保持嵌套導航器定義的初始路由不變(initialRouteName
的值)
當你指定導航到嵌套導航器的某一屏時(navigation.navigate('Root', { screen: 'Settings' });
),導航器定義初始路由就會被替換成這一屏,也就是說,下次直接導航這個嵌套導航器的時候(navigation.navigate('Root');
),會默認顯示這個Screen(Settings
)。
如果不想初始路由被改變,我們就要在跳轉的時候加一個initial: false,
,如下:
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
六、盡量避免深度嵌套
在能實現(xiàn)需求的基礎上,請盡可能地少嵌套導航棧,建議層數(shù)最多不要超過兩層。因為這會有很多副作用。
比如會引起低端設備的內存和性能問題。
影響代碼可讀性,過于冗余復雜變得難以維護。
Tab里再放Tab,Drawer里再放Drawer,會帶來不好的用戶體驗。
如果你為了代碼邏輯更清晰,想為Navigtor下的Screen分類,可以考慮像這樣做:
// Define multiple groups of screens in objects like this
const commonScreens = {
Help: HelpScreen,
};
const authScreens = {
SignIn: SignInScreen,
SignUp: SignUpScreen,
};
const userScreens = {
Home: HomeScreen,
Profile: ProfileScreen,
};
// Then use them in your components by looping over the object and creating screen configs
// You could extract this logic to a utility function and reuse it to simplify your code
<Stack.Navigator>
{Object.entries({
// Use the screens normally
...commonScreens,
// Use some screens conditionally based on some condition
...(isLoggedIn ? userScreens : authScreens),
}).map(([name, component]) => (
<Stack.Screen name={name} component={component} />
))}
</Stack.Navigator>;
七、使用嵌套導航器,其他要注意并弄清的點
(1) 每個導航器保管它自己的導航歷史
比如,當你在一個被嵌套在Screen里的堆棧導航器上點擊返回按鈕的時候,它會返回到本導航器(就是被嵌套的stack導航器)導航歷史中的上一頁,而不是返回到上級導航器中。
(2) 每個導航器中的屏幕有它自己的參數(shù)
比如,傳遞給嵌套導航器中的screen的任何參數(shù)都在該屏幕的route prop中,且不能被它的父或子導航器中的屏幕訪問。
如果要從子屏幕訪問父屏幕的參數(shù),可以使用React Context
將參數(shù)暴露給子屏幕。
(3) 導航action會優(yōu)先由當前導航器處理,如果當前導航器不能處理則通過冒泡的方式由上一級導航器處理
比如,你在一個被嵌套的導航器的screen中調用navigation.goBack()
,那么只有當你在該導航器的首頁時你才會返回到父導航器中。其他的action像navigate
工作原理相同。也就是說,只有當被嵌套的導航器不能處理這個action時,父導航器才會試圖去處理它。
在上面的例子(首頁為底部tab欄的典型嵌套結構)中,當你在Feed
頁調用navigate('Messages')
,嵌套的tab導航器會處理這個action,但當你在這里調用navigate('Settings')
,就會由它的父導航器來處理了。
(4) 導航器的一些特定方法可以在子導航器中使用
比如,如果一個stack導航器嵌套在drawer導航器中,那么drawer導航器的openDrawer
、closeDrawer
、toggleDrawer
等方法在被嵌套的stack導航器傳遞給屏幕的navigation屬性中依然是可用的。但是如果stack導航器是drawer的父導航器,那么它里面的screen是不能訪問這些方法的,因為它沒有被嵌套在drawer導航器里。
同樣,如果一個tab導航器被嵌套在stack導航器中,那么tab導航器screen中的navigation屬性會新得到push
和replace
這兩個方法。
如果你想從父導航器中分派動作給嵌套的子導航器,可以使用 navigation.dispatch
具體語句:navigation.dispatch(DrawerActions.toggleDrawer());
(5) 被嵌套的導航器不會響應父級導航器的事件
比如說,你有一個嵌套在tab導航器中的stack導航器,那么stack導航器的screen在用navigation.addListener
綁定監(jiān)聽事件時,不能接收到由父tab導航器觸發(fā)出的事件,比如tabPress
。為了能夠響應父導航器的事件,你可以用navigation.dangerouslyGetParent().addListener
來顯式地監(jiān)聽父級導航器事件。
useEffect(() => {
const unsubscribe = navigation
.dangerouslyGetParent()
.addListener('tabPress', (e) => {
// ...
});
return unsubscribe;
}, [navigation]);
(6) 父級導航器的UI先于子導航器被渲染
例如,將stack導航器嵌套在drawer導航器內部時,你會看到drawer顯示在stack導航器標題的上方。但是如果將drawer導航器嵌套在stack導航器中,則drawer將出現(xiàn)在stack標題下方。這是在決定如何嵌套導航器時要考慮的一個要點。
在開發(fā)應用時,你可能會根據(jù)需求來選用下面這些模式:
- 在根stack導航器的首屏嵌套tab導航器——當你通過push跳轉頁面的時候,新的頁面會覆蓋掉標簽欄。
- 在drawer導航器的每個頁面嵌套stack導航器----即先渲染抽屜效果再渲染stack導航器的頭部
- tab導航器的每個頁面都嵌套stack導航器----tab導航器的標簽欄仍然可見。常見的就是點擊tab將stack置頂。
詳細官方說明:Nesting navigators