为了阅读体验,分为上下两篇。算不上教程,主要目的还是摘取使用这个库时的常用知识点和解决方案,便于自己记忆和查阅。
本篇梳理React Navigation 5.x 的一些基础API、嵌套导航注意事项、如何设计合理的嵌套路由等。
第二篇主要讲如何实现我自身App的两个需求:1、在嵌套路由中动态配置顶部标题栏(tab导航器嵌套在根部stack导航器的首屏);2、监听tab点击事件,触发时将对应screen重置到初始状态(数据)。会把自己的代码结构放出来。React Navigation 5.x(二)嵌套路由动态配置标题栏及自定义tabbar点击事件
循序渐进,我们先看一些干货,最后再来实现这两个功能。
一、堆栈导航器Navigator和Screen的具体参数配置及说明
这边只列举比较实用的。如有需要可以去官网查阅API:createStackNavigator
1. 导航器组件Navigator常用参数(Props):
参数名 | 说明 |
---|---|
initialRouteName | 导航器初次加载时要渲染的路由的名字,对应Screen(屏幕组件)的name ,默认渲染该栈内第一个Sreen |
screenOptions | 为该栈内所有Sreen配置通用属性,即可把Screen的options里的属性提到这里统一设置。当这些Screen有相同的属性时,没有必要复制多份并在它们的options上重复设置。相同的属性,Screen的options里配置的优先级更高。 |
keyboardHandlingEnabled | 默认值为true ,若设置为false ,屏幕上的键盘将不会在导航到新屏幕时自动消失。 |
headerMode | 设置该栈标题栏的形式 'float' '(ios模式)|'screen' (Android模式)|'none' (不显示头部标题栏) |
2. 导航器内的屏幕组件Screen常用参数(统一在options里配置):
参数名 | 类型 | 说明 |
---|---|---|
title | String |
Screen标题栏的文字 |
header | Function |
返回一个React Element作为自定义标题栏。要使用这个配置,必须先确保设置Navigator的headerMode:'screen' ,以及定义好标题栏高度,e.g. headerStyle: { height: 80 } 参考示例(1)
|
headerTitle | String | Function |
如果是函数,返回一个接收参数的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 = '自定义标题' ;
// 这边标题一般都是我们通过获取路由参数再经过方法判断确定的,这里写死为了方便演示如何把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
能执行的方法是有区别的:
这边只展开讲了部分方法,每个蓝色Action
都加了API直达链接。
比如我们有个导航屏幕:
<Stack.Screen key="Profile-1" name="Profile" component={component} />
-
通用方法:
-
navigate - 导航到特定路由,参数必须包含name或key属性,可选参数params(合并到目标路由的参数)。通俗理解就是转到这个name/key对应的路由。如果正在当前路由屏幕组件页面,那么是不会有任何反应的。
格式:navigation.navigate(name: string, [params: object])
navigation.navigate( 'Profile', { user: 'jane' })
-
goBack - 返回导航堆栈历史记录的上一个屏幕。
navigation.goBack()
-
reset - 可以让我们用新的导航状态替换当前的导航状态,即移除现有的已经入栈的屏幕和历史记录,设置新的入栈屏幕们。如果希望在更改状态时保留现有的屏幕,可以使用
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 - 更新当前或特定路由的参数。 作用就像React的setState,传入的参数和旧的params合并对象而非覆盖。
如果要为特定路由更新参数,则可添加source
属性,值为该路由的key。
import { CommonActions } from '@react-navigation/native'; navigation.dispatch({ ...CommonActions.setParams({ user: 'Wojtek' }), source: route.key, });
- setOptions 可以在屏幕组件内部,根据它的props、state、context,来定制我们的屏幕组件选项(screen options),比如title等。
-
navigate - 导航到特定路由,参数必须包含name或key属性,可选参数params(合并到目标路由的参数)。通俗理解就是转到这个name/key对应的路由。如果正在当前路由屏幕组件页面,那么是不会有任何反应的。
-
堆栈导航器:
- replace - 用新路由替换导航状态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 });
-
tab选项卡导航器:
-
jumpTo - 跳转至tab导航器中的现有路由。
格式:navigation.jumpTo(name: string, [params: object])
-
jumpTo - 跳转至tab导航器中的现有路由。
-
drawer抽屉导航器:
-
jumpTo - 跳转至drawer导航器中的现有路由。格式同tab导航器的
jumpTo
-
openDrawer - 打开drawer导航器面板。格式:
navigation.openDrawer()
-
closeDrawer - 关闭drawer面板。格式:
navigation.closeDrawer()
-
toggleDrawer - 切换drawer面板开关状态。格式:
navigation.toggleDrawer()
-
jumpTo - 跳转至drawer导航器中的现有路由。格式同tab导航器的
-
高级API参考
-
dispatch - dispatch方法允许我们发送一个导航动作对象(包含用于生成特定基于某类型导航器的操作方法),来确定导航状态如何更新。除非实在无法直接通过navigate,goBack等方法完成我们所需的操作。不然应该避免使用它。尽量都通过
navigation.[普通方法]
属性来导航。
dispatch可调度的对象除了CommonActions,还有StackActions 、还有DrawerActions 、还有TabActions 。这仨都扩展于CommonActions。
-
dispatch - dispatch方法允许我们发送一个导航动作对象(包含用于生成特定基于某类型导航器的操作方法),来确定导航状态如何更新。除非实在无法直接通过navigate,goBack等方法完成我们所需的操作。不然应该避免使用它。尽量都通过
// 要先获取特定的导航动作创造器
import { CommonActions } from '@react-navigation/native';
navigation.dispatch(
// 再去触发方法
CommonActions.navigate({
name: 'Profile',
params: {
user: 'jane',
},
})
);
三、导航状态Navigation state
navigation state是React Navigation存储应用程序的路由结构和历史记录的对象。
比如,在主屏幕嵌套了一个标签导航器的堆栈导航器,可能具有如下导航状态:
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,
};
每个导航状态对象中包含的属性:
-
type
-这个导航状态归属的导航器的类型,例如stack
,tab
,drawer
。 -
key
-识别导航器的唯一键。 -
routeName
-包含所属导航器的每个屏幕name
(字符串)的数组。 -
routes
-在导航器中呈现的路由对象(屏幕)的列表。它还在堆栈导航器中表示历史记录。此数组中至少应存在一项。 -
index
-正获得焦点的路由对象在routes
数组中的索引。 -
history
-访问过的项目列表。这是一个可选属性,并非在所有导航器中都存在。比如它仅存在于核心的tab和抽屉导航器中。history
数组中的项目可以根据导航器而变化。此数组中至少应存在一项。 -
stale
-除非显式设置了stale
属性,否则值默认是false
。也就表示导航状态对象需要“自动补齐”。
routes
数组中的每个路由对象(route
①)都可以包含以下属性:
-
key
-屏幕的唯一键。会自动创建或在导航到此屏幕时添加。 -
name
-屏幕名称。在导航器组件层次结构中定义。 -
params
-可选,一个包含参数的对象,有导航动作时定义,例如navigate('Home', { sortBy: 'latest' })
。 -
state
-可选,嵌套在此屏幕内的子导航器的导航状态对象。只会在导航事件发生后才挂到路由对象上。
①注:每个screen组件的props.route
,就是上面说的routes
数组中的路由对象,内容为这个屏幕的路由数据。
四、设计合理的导航结构
嵌套导航器就是在一个Navigator的一个Screen里渲染的Navigator,作为一个组件元素赋给Screen的component属性。
一个应用通常都拥有底部选项卡(tabbar
),一般是主页的标准配置。同时应用中的部分页面(比如登录页等)不需要tabbar。要实现这一点,能从导航结构入手就不要去动态设置隐藏/显示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' });
如果你要带参数跳转,现在就得这样做:
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,
});
六、尽量避免深度嵌套
在能实现需求的基础上,请尽可能地少嵌套导航栈,建议层数最多不要超过两层。因为这会有很多副作用。
比如会引起低端设备的内存和性能问题。
影响代码可读性,过于冗余复杂变得难以维护。
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) 每个导航器中的屏幕有它自己的参数
比如,传递给嵌套导航器中的screen的任何参数都在该屏幕的route prop中,且不能被它的父或子导航器中的屏幕访问。
如果要从子屏幕访问父屏幕的参数,可以使用React Context
将参数暴露给子屏幕。
(3) 导航action会优先由当前导航器处理,如果当前导航器不能处理则通过冒泡的方式由上一级导航器处理
比如,你在一个被嵌套的导航器的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
绑定监听事件时,不能接收到由父tab导航器触发出的事件,比如tabPress
。为了能够响应父导航器的事件,你可以用navigation.dangerouslyGetParent().addListener
来显式地监听父级导航器事件。
useEffect(() => {
const unsubscribe = navigation
.dangerouslyGetParent()
.addListener('tabPress', (e) => {
// ...
});
return unsubscribe;
}, [navigation]);
(6) 父级导航器的UI先于子导航器被渲染
例如,将stack导航器嵌套在drawer导航器内部时,你会看到drawer显示在stack导航器标题的上方。但是如果将drawer导航器嵌套在stack导航器中,则drawer将出现在stack标题下方。这是在决定如何嵌套导航器时要考虑的一个要点。
在开发应用时,你可能会根据需求来选用下面这些模式:
- 在根stack导航器的首屏嵌套tab导航器——当你通过push跳转页面的时候,新的页面会覆盖掉标签栏。
- 在drawer导航器的每个页面嵌套stack导航器----即先渲染抽屉效果再渲染stack导航器的头部
- tab导航器的每个页面都嵌套stack导航器----tab导航器的标签栏仍然可见。常见的就是点击tab将stack置顶。
详细官方说明:Nesting navigators