导航器嵌套
导航器嵌套意味着在一个导航器的屏幕中渲染另一个导航器,例如
- 静态
- 动态
const HomeTabs = createBottomTabNavigator({
screens: {
Feed: FeedScreen,
Messages: MessagesScreen,
},
});
const RootStack = createNativeStackNavigator({
screens: {
Home: {
screen: HomeTabs,
options: {
headerShown: false,
},
},
Profile: ProfileScreen,
},
});
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Messages" component={MessagesScreen} />
</Tab.Navigator>
);
}
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeTabs}
options={{ headerShown: false }}
/>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
在上面的例子中,HomeTabs
包含一个标签导航器。它也被用于 RootStack
中的堆栈导航器的 Home
屏幕。所以在这里,一个标签导航器被嵌套在一个堆栈导航器中
RootStack
(堆栈导航器)HomeTabs
(标签导航器)Feed
(屏幕)Messages
(屏幕)
Profile
(屏幕)
导航器嵌套的工作方式非常像嵌套常规组件。为了实现你想要的行为,通常需要嵌套多个导航器。
导航器嵌套如何影响行为
当嵌套导航器时,有一些事情需要记住
每个导航器都保留自己的导航历史记录
例如,当你在嵌套堆栈导航器中的屏幕内按下返回按钮时,即使父级是另一个导航器,它也会返回到嵌套堆栈内的上一个屏幕。
每个导航器都有自己的选项
例如,在子导航器中嵌套的屏幕中指定 title
选项不会影响父导航器中显示的标题。
如果你想实现这种行为,请参阅 嵌套导航器的屏幕选项 指南。如果你在堆栈导航器中渲染标签导航器,并希望在堆栈导航器的头部显示标签导航器中活动屏幕的标题,这将非常有用。
导航器中的每个屏幕都有自己的参数
例如,传递给嵌套导航器中屏幕的任何 params
都位于该屏幕的 route
对象中,并且无法从父导航器或子导航器中的屏幕访问。
如果你需要从子屏幕访问父屏幕的参数,你可以使用 React Context 将参数暴露给子组件。
导航操作由当前导航器处理,如果无法处理,则会冒泡
例如,如果你在嵌套屏幕中调用 navigation.goBack()
,只有当你已经位于导航器的第一个屏幕时,它才会返回到父导航器。其他操作(如 navigate
)的工作方式类似,即导航将在嵌套导航器中发生,如果嵌套导航器无法处理,则父导航器将尝试处理。在上面的例子中,当在 Feed
屏幕内调用 navigate('Messages')
时,嵌套的标签导航器将处理它,但如果你调用 navigate('Settings')
,父堆栈导航器将处理它。
导航器特定的方法在嵌套在内部的导航器中可用
例如,如果你的抽屉导航器内部有一个堆栈,则抽屉的 openDrawer
、closeDrawer
、toggleDrawer
等方法也将在堆栈导航器内部屏幕的 navigation
对象上可用。但是,假设你有一个堆栈导航器作为抽屉的父级,那么堆栈导航器内部的屏幕将无法访问这些方法,因为它们没有嵌套在抽屉内部。
同样,如果你的堆栈导航器内部有一个标签导航器,则标签导航器中的屏幕将在其 navigation
对象中获得堆栈的 push
和 replace
方法。
如果你需要从父级向嵌套的子导航器分发操作,你可以使用 navigation.dispatch
navigation.dispatch(DrawerActions.toggleDrawer());
嵌套导航器不接收父级的事件
例如,如果你的标签导航器内部嵌套了一个堆栈导航器,则堆栈导航器中的屏幕将不会接收父标签导航器发出的事件,例如使用 navigation.addListener
时的 (tabPress
)。
要接收来自父导航器的事件,你可以使用 navigation.getParent
显式监听父级的事件
- 静态
- 动态
const unsubscribe = navigation
.getParent('MyTabs')
.addListener('tabPress', (e) => {
// Do something
alert('Tab pressed!');
});
const unsubscribe = navigation
.getParent('MyTabs')
.addListener('tabPress', (e) => {
// Do something
alert('Tab pressed!');
});
这里的 'MyTabs'
指的是你在要监听其事件的父标签导航器的 id
中传递的值。
父导航器的 UI 在子导航器的顶部渲染
例如,当你在抽屉导航器内部嵌套一个堆栈导航器时,你会看到抽屉出现在堆栈导航器的头部上方。但是,如果你将抽屉导航器嵌套在堆栈内部,则抽屉将出现在堆栈的头部下方。这是在决定如何嵌套导航器时要考虑的重要一点。
在你的应用程序中,你可能会根据你想要的行为使用这些模式
- 标签导航器嵌套在堆栈导航器的初始屏幕内 - 当你推送新屏幕时,新屏幕会覆盖标签栏。
- 抽屉导航器嵌套在堆栈导航器的初始屏幕内,且初始屏幕的堆栈头部隐藏 - 抽屉只能从堆栈的第一个屏幕打开。
- 堆栈导航器嵌套在抽屉导航器的每个屏幕内 - 抽屉出现在堆栈的头部上方。
- 堆栈导航器嵌套在标签导航器的每个屏幕内 - 标签栏始终可见。通常,再次按下标签也会将堆栈弹出到顶部。
导航到嵌套导航器中的屏幕
考虑以下示例
- 静态
- 动态
const MoreTabs = createBottomTabNavigator({
screens: {
Feed: FeedScreen,
Messages: MessagesScreen,
},
});
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
More: {
screen: MoreTabs,
options: {
headerShown: false,
},
},
},
});
function MoreTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Messages" component={MessagesScreen} />
</Tab.Navigator>
);
}
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="More"
component={MoreTabs}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
}
在这里,你可能想从你的 HomeScreen
组件导航到 More
屏幕(其中包含 MoreTabs
)
navigation.navigate('More');
它有效,并且显示了 MoreTabs
组件内部的初始屏幕,即 Feed
。但有时你可能想控制导航时应显示的屏幕。为了实现这一点,你可以在 params 中传递屏幕名称
navigation.navigate('More', { screen: 'Messages' });
现在,Messages
屏幕将在导航时渲染,而不是 Feed
。
向嵌套导航器中的屏幕传递参数
你还可以通过指定 params
键来传递参数
- 静态
- 动态
navigation.navigate('More', {
screen: 'Messages',
params: { user: 'jane' },
})
navigation.navigate('More', {
screen: 'Messages',
params: { user: 'jane' },
})
如果导航器已经渲染,导航到另一个屏幕将在堆栈导航器的情况下推送一个新屏幕。
你可以对深度嵌套的屏幕采用类似的方法。请注意,此处 navigate
的第二个参数只是 params
,因此你可以执行以下操作
navigation.navigate('Home', {
screen: 'Settings',
params: {
screen: 'Sound',
params: {
screen: 'Media',
},
},
});
在上面的例子中,你正在导航到 Media
屏幕,该屏幕在一个嵌套在 Sound
屏幕内的导航器中,而 Sound
屏幕又在一个嵌套在 Settings
屏幕内的导航器中。
screen
和相关参数保留供内部使用,并由 React Navigation 管理。虽然你可以在父屏幕中访问 route.params.screen
等,但依赖它们可能会导致意外行为。
渲染导航器中定义的初始路由
默认情况下,当你在嵌套导航器中导航屏幕时,指定的屏幕将用作初始屏幕,并且导航器上的 initialRouteName
prop 将被忽略。
如果你需要渲染导航器中指定的初始路由,你可以通过设置 initial: false
来禁用将指定屏幕用作初始屏幕的行为
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
这会影响按下返回按钮时发生的情况。当有初始屏幕时,返回按钮会将用户带到那里。
避免嵌套时出现多个头部
当嵌套多个堆栈、抽屉或底部标签导航器时,将显示来自子导航器和父导航器的头部。但是,通常更希望在子导航器中显示头部,并在父导航器的屏幕中隐藏头部。
为了实现这一点,你可以使用 headerShown: false
选项在包含导航器的屏幕中隐藏头部。
例如
- 静态
- 动态
const HomeTabs = createBottomTabNavigator({
screens: {
Feed: FeedScreen,
Messages: MessagesScreen,
},
});
const RootStack = createNativeStackNavigator({
screens: {
Home: {
screen: HomeTabs,
options: {
headerShown: false,
},
},
Profile: ProfileScreen,
},
});
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Messages" component={MessagesScreen} />
</Tab.Navigator>
);
}
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeTabs}
options={{
headerShown: false,
}}
/>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
在这些示例中,我们使用了直接嵌套在另一个堆栈导航器中的底部标签导航器,但当中间有其他导航器时,相同的原则也适用,例如:堆栈导航器内部的标签导航器,而标签导航器又在另一个堆栈导航器内部,堆栈导航器内部的抽屉导航器等。
如果你不希望任何导航器中出现头部,你可以在所有导航器中指定 headerShown: false
- 静态
- 动态
const HomeTabs = createBottomTabNavigator({
screenOptions: {
headerShown: false,
},
screens: {
Feed: FeedScreen,
Messages: MessagesScreen,
},
});
const RootStack = createStackNavigator({
screenOptions: {
headerShown: false,
},
screens: {
Home: HomeTabs,
Profile: ProfileScreen,
},
});
function HomeTabs() {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
}}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Messages" component={MessagesScreen} />
</Tab.Navigator>
);
}
function RootStack() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="Home" component={HomeTabs} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
嵌套时的最佳实践
我们建议将导航器嵌套减少到最少。尽量用尽可能少的嵌套来实现你想要的行为。嵌套有很多缺点
- 它会导致深度嵌套的视图层次结构,这可能会在低端设备中导致内存和性能问题
- 嵌套相同类型的导航器(例如,标签页内的标签页,抽屉内的抽屉等)可能会导致令人困惑的 UX
- 过度嵌套会导致代码在导航到嵌套屏幕、配置深度链接等时变得难以理解。
将导航器嵌套视为实现你想要的 UI 的一种方式,而不是组织代码的方式。如果你想为组织创建单独的屏幕组,而不是使用单独的导航器,你可以使用 Group
组件进行动态配置,或使用 groups
属性 进行静态配置。
- 静态
- 动态
const MyStack = createStackNavigator({
screens: {
// Common screens
},
groups: {
// Common modal screens
Modal: {
screenOptions: {
presentation: 'modal',
},
screens: {
Help,
Invite,
},
},
// Screens for logged in users
User: {
if: useIsLoggedIn,
screens: {
Home,
Profile,
},
},
// Auth screens
Guest: {
if: useIsGuest,
screens: {
SignIn,
SignUp,
},
},
},
});
<Stack.Navigator>
{isLoggedIn ? (
// Screens for logged in users
<Stack.Group>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Profile" component={Profile} />
</Stack.Group>
) : (
// Auth screens
<Stack.Group screenOptions={{ headerShown: false }}>
<Stack.Screen name="SignIn" component={SignIn} />
<Stack.Screen name="SignUp" component={SignUp} />
</Stack.Group>
)}
{/* Common modal screens */}
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="Help" component={Help} />
<Stack.Screen name="Invite" component={Invite} />
</Stack.Group>
</Stack.Navigator>