react navigation7.x 中文文档青春极速mini版-Fundamentals
0. 术语表
-
Navigator 导航器。导航器是一个 React 组件,决定如何渲染您定义的屏幕。它包含 Screen 元素作为其子元素,以定义屏幕的配置。
-
NavigationContainer 一个组件。用于管理我们的导航树并包含导航状态。此组件必须包装所有导航器结构。通常情况下,我们会在应用程序的根组件中渲染此组件,通常是从 App.js 导出的组件。
-
Router Router 是一组函数,用于决定如何处理导航器中的操作和状态更改(类似于 Redux 应用程序中的 reducer)。通常情况下,您不需要直接与 Router 交互,除非您正在编写自定义导航器。
-
Screen Component 屏幕组件。屏幕组件是我们在路由配置中使用的组件。
const Stack = createNativeStackNavigator();
const StackNavigator = (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen} // <----
/>
<Stack.Screen
name="Details"
component={DetailsScreen} // <----
/>
</Stack.Navigator>
);
组件名称中的后缀 Screen
是完全可选的,但是经常使用的约定;我们可以将其称为 Michael
,这仍然可以正常工作。
我们之前看到,我们的屏幕组件会提供 navigation prop。重要的是,只有在屏幕作为路由被 React Navigation 渲染时(例如响应于 navigation.navigate),才会发生这种情况。
例如,如果我们将 DetailsScreen 渲染为 HomeScreen 的子组件,则不会为 DetailsScreen 提供 navigation prop,当您在 Home 屏幕上按下“再次转到详细信息...”按钮时,应用程序将抛出经典的 JavaScript 异常“undefined is not an object”。
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details')}
/>
<DetailsScreen />
</View>
);
}
导航属性参考 部分详细介绍了此问题,描述了解决方法,并提供了有关 navigation prop 上其他可用属性的更多信息。
-
Navigation Prop 导航属性, 此 prop 将传递给所有屏幕,可以用于以下操作:
dispatch
将向路由器发送一个 actionnavigate
、goBack
等可用于方便地调度操作
导航器也可以接受 navigation prop,如果有父级导航器,则应从父级导航器获取它。
有关更多详细信息,请参见导航属性文档。
路由属性参考 部分详细介绍了此问题,描述了解决方法,并提供了有关 route prop 上其他可用属性的更多信息。
-
Route Prop 路由属性
此 prop 将传递给所有屏幕。包含有关当前路由的信息,例如 params、key 和 name。
-
Navigation State 导航状态, 导航器的状态通常如下所示:
{
key: 'StackRouterRoot',
index: 1,
routes: [
{ key: 'A', name: 'Home' },
{ key: 'B', name: 'Profile' },
]
}
对于此导航状态,有两个路由(可以是选项卡或堆栈中的卡片)。索引指示活动路由,即“B”。
您可以在此处阅读有关导航状态的更多信息。
-
Route 路由
每个路由都是一个对象,包含用于标识它的键和用于指定路由类型的“名称”。它还可以包含任意参数。
{
key: 'B',
name: 'Profile',
params: { id: '123' }
}
-
Header 标题
也称为导航标题、导航栏、应用栏,可能还有许多其他名称。这是屏幕顶部的矩形,其中包含返回按钮和屏幕标题。整个矩形通常在 React Navigation 中称为标题。
-
StackNavigator
- 为应用程序提供了一种页面切换的方法,每次切换时,新的页面会放置在堆栈的顶部 -
TabNavigator
- 用于设置具有多个Tab页的页面 -
DrawerNavigator
- 用于设置抽屉导航的页面
1. 开始
1.1 基本要求
react-native
>= 0.63.0expo
>= 41 (if you use Expo)typescript
>= 4.1.0 (if you use TypeScript)
1.2 安装依赖
yarn add @react-navigation/native
yarn add react-native-screens react-native-safe-area-context
可选:使用 expo 创建的项目
npx expo install react-native-screens react-native-safe-area-context
提示:只要程序能够正常构建你可以忽略由 peer dependencies 引起的警告
对于 React Native 0.60 版本或更高,无需再运行 react-native link
对于 Mac 用户 IOS 开发, 需要安装 pods (通过 Cocoapods)以完成链接
npx pod-install ios
对于 Android 开发需要一些额外的配置
编辑 android/app/src/main/java/<your package name>/MainActivity.java
文件
+ import android.os.Bundle;
public class MainActivity extends ReactActivity {
// ...
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(null);
+ }
// ...
}
这一变化是必要的,以避免与视图状态在活动重启时不一致而导致的崩溃。
注意:当你使用一个导航器(如 Stack.Navigator )时,你需要按照该导航器的安装说明来安装任何额外的依赖项。如果报错 "Unable to resolve module",你需要在你的项目中安装该模块。
1.3 使用 NavigationContainer
包裹 app
// App.js
import * as React from 'react'
import { NavigationContainer } from '@react-navigation/native'
export default function App() {
return <NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
}
注意: NavigationContainer 应该只有一个,除非用例特殊,你不应该嵌套它
2. Hello React Navigation
React Native 没有内置类似 web 的的全局历史堆栈概念, React Navigation 提供了这些
React Navigation 带有三种默认的 navigator。
Stack.Navigator
- 为应用程序提供了一种页面切换的方法,每次切换时,新的页面会放置在堆栈的顶部Tab.Navigator
- 用于设置具有多个 Tab 页的页面Drawer.Navigator
- 用于设置抽屉导航的页面
React Navigation 的本地 Stack.Navigator 提供了一种方法,使您的应用程序可以在屏幕之间进行转换并管理导航历史记录。如果您的应用程序仅使用一个 Stack.Navigator ,则在概念上类似于 Web 浏览器处理导航状态 - 您的应用程序在用户与其交互时从导航堆栈中推送和弹出项目,这会导致用户看到不同的屏幕。这在 Web 浏览器和 React Navigation 中的工作方式之间的一个关键区别是,React Navigation 的本地 Stack.Navigator 提供了您在 Android 和 iOS 上导航到堆栈中的路由时所期望的手势和动画。
2.1 安装 @react-navigation/native-stack
到目前为止,我们所安装的库是导航器的构建模块和共享基础,React Navigation 中的每个导航器都在自己的库中。要使用本地 Stack.Navigator ,我们需要安装 @react-navigation/native-stack
yarn add @react-navigation/native-stack
@react-navigation/native-stack
依赖于react-native-screens
2.2 createNativeStackNavigator
& NavigationContainer
createNativeStackNavigator
函数返回一个包含 Screen
和 Navigator
属性的对象。它们都是用于配置导航器的 React 组件。导航器应该包含屏幕元素作为其子代,以定义路由的配置。
NavigationContainer
是一个管理我们的导航树并包含导航状态的组件。这个组件必须包住所有的导航器结构。通常,我们会在应用程序的根部渲染这个组件,这通常是由 App.js 导出的组件。
// In App.js in a new project
import * as React from 'react'
import { View, Text } from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
)
}
const Stack = createNativeStackNavigator()
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
)
}
export default App
导航条和内容区的样式是 Stack.Navigator 的默认配置
现在来添加第二个页面
function DetailsScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
</View>
)
}
const Stack = createNativeStackNavigator()
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName='Home'>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
)
}
initialRouteName
指定了初始路由
现在我们有两个路由: Home
和 Detail
, 路由可以通过 Stack.Screen
组件指定,
name
属性对应路由名称,component
属性对应将要渲染的组件
注意:组件 props 接受的是组件,而不是渲染函数。不要传递内联函数(例如:
component={() => <HomeScreen />})
,否则当父级组件重新渲染时,你的组件会卸载-重新挂载且失去所有的状态。参见传递额外的 props以了解替代方案。
2.3 指定选项
导航器中的每个 Screen
都可以为导航器指定一些选项,比如要在 Header 中呈现的标题。这些选项可以在每个屏幕组件的 props 中传递:
<Stack.Screen name='Home' component={HomeScreen} options={{ title: 'Overview' }} />
如果想要为所有 screen 传递相同的选项,使用 screenOptions
2.4 传递额外的属性
两种方法
- 使用
React Context
- 使用 Render Props
<Stack.Screen name='Home'>{props => <HomeScreen {...props} extraData={someData} />}</Stack.Screen>
注意:默认情况下,React Navigation 会对屏幕组件进行优化,以防止不必要的渲染。使用 Render Props 会移除这些优化。因此,如果你使用它,你需要确保你的
Screen
组件使用React.memo
或React.PureComponent
以避免性能问题。
3. 路由跳转
在 web 浏览器中路由跳转的写法可能是
<a href="details.html">Go to Details</a>
或者
<a onClick={() => { window.location.href = 'details.html'; }}>
Go to Details
</a>
而 React Navigation 中则使用传递到 screen 组件中的 navigation
属性
import { Button, View, Text } from 'react-native'
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button title='Go to Details' onPress={() => navigation.navigate('Details')} />
</View>
)
}
navigation
属性传递到导航器下每个 screen 组件中- 使用
navigation.navigate('Details')
跳转
如果跳转一个未定义的路由名称,在开发构建时会打印一个错误,生产构建中什么都不会发生
3.1 navgate
& push
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button title='Go to Details... again' onPress={() => navigation.navigate('Details')} />
</View>
)
}
如果你运行这段代码,你会注意到,当你点击 "Go to Details... again"时,它并没有做任何事情!这是因为我们已经进入了详细信息路线!这是因为我们已经在 Details 路由上了。navigate
的意思是 "去这个屏幕",如果你已经在那个屏幕上,那么它什么也不做也是合理的。
让我们假设一下,我们实际上想添加另一个 Details 界面。这在你向路由传递一些独特的数据的情况下是很常见的(稍后在我们谈论参数时,会有更多关于这个问题的介绍!)。要做到这一点,我们可以把 navigate
改为 push
。这允许我们表达添加另一路由的意图,而不考虑现有的导航历史。
<Button title='Go to Details... again' onPress={() => navigation.push('Details')} />
navigate
和 push
: 它们的区别在于
- 如果已经加载了页面,navigate 方法将跳转到已经加载的页面,而不会重新创建一个新的页面
- 而 push 方法则会在堆栈顶部添加一条路由,并导航至该路由
3.2 goBack()
当可以从活动屏幕返回时,本地 Stack.Navigator 提供的标题将自动包括一个返回按钮(如果导航堆栈中只有一个屏幕,就没有可以返回的东西,所以没有返回按钮) goBack()
可以编程方式触发返回这一行为
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button title='Go to Details... again' onPress={() => navigation.push('Details')} />
<Button title='Go to Home' onPress={() => navigation.navigate('Home')} />
<Button title='Go back' onPress={() => navigation.goBack()} />
</View>
)
}
在安卓系统中,React Navigation 与硬件后退按钮挂钩,当用户按下后退按钮时为你启动
goBack()
函数,因此它的行为与用户所期望的一样。
返回堆栈中第一个屏幕
// 回到 Home 页
<Button title='Go to Home' onPress={() => navigation.navigate('Home')} />
// 回到堆栈中第一个屏幕
<Button title='Go back to first screen in stack' onPress={() => navigation.popToTop()} />
4. 传递参数
传递参数需要两个步骤:
- 作为
navigate
函数的第二个参数navigation.navigate('RouteName', { /* params */})
- 在你的 screen 组件中读取参数:
route.params
我们建议你传递的参数是可序列化的 JSON。这样,你就可以使用状态持久化,你的 screen 组件将正确实现 Deep Linking。
Deep Linking 是一种优化用户体验的策略,它允许您通过 URL 直接导航到应用程序的特定屏幕。React Native 提供了 Linking 来获取传入链接的通知。React Navigation 可以与 Linking 模块集成,以自动处理深度链接。在 Web 上,React Navigation 可以与浏览器的历史记录 API 集成,以处理客户端上的 URL
示例: 传递
onPress = {() => {
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
})
}}
示例: 读取
function DetailsScreen({ route, navigation }) {
const { itemId, otherParam } = route.params
// ...
}
4.1 传递初始参数
如果你在导航到这个屏幕时没有指定任何参数,初始参数将被使用。它们也会与你传递的任何参数进行浅层合并。初始参数用一个initialParams
来指定
<Stack.Screen name='Details' component={DetailsScreen} initialParams={{ itemId: 42 }} />
4.2 更新参数
屏幕也可以更新它们的参数,就像它们可以更新它们的状态一样。navigation.setParams
方法让你更新屏幕的参数。更多细节请参考 setParams 的 API 参考。
navigation.setParams({ query: 'someText' })
注意:避免使用
setParams
来更新屏幕选项,如 title 等。如果你需要更新选项,请使用setOptions
代替。
4.3 将参数传递给前一个屏幕
假设你有一个带有创建帖子按钮的屏幕,而创建帖子按钮打开了一个新的屏幕来创建一个帖子。在创建帖子后,你想把帖子的数据传回给前一个屏幕。
为了实现这一点,你可以使用 navigate
方法,如果屏幕已经存在,它的作用类似于 goBack
。你可以通过 navigate
传递参数来把数据传回去:
function HomeScreen({ navigation, route }) {
React.useEffect(() => {
if (route.params?.post) {
// Post updated, do something with `route.params.post`
// For example, send the post to the server
}
}, [route.params?.post])
return (
<View>
<Button title='Create post' onPress={() => navigation.navigate('CreatePost')} />
<Text style={{ margin: 10 }}>Post: {route.params?.post}</Text>
</View>
)
}
function CreatePostScreen({ navigation, route }) {
const [postText, setPostText] = React.useState('')
return (
<>
<TextInput
multiline
placeholder="What's on your mind?"
style={{ height: 200, padding: 10, backgroundColor: 'white' }}
value={postText}
onChangeText={setPostText}
/>
<Button
title='Done'
onPress={() => {
// Pass and merge params back to home screen
navigation.navigate({
name: 'Home',
params: { post: postText },
merge: true,
})
}}
/>
</>
)
}
4.4 将参数传递给嵌套的导航器
例如,假设你在 Account
屏幕内有一个导航器,并想把参数传给该导航器内的 Settings
屏幕。那么你可以按以下方式传递参数:
navigation.navigate('Account', {
screen: 'Settings',
params: { user: 'jane' },
})
4.5 参数中应该有什么
参数就像屏幕的选项。它们应该只包含配置屏幕中显示的信息。避免传递将在屏幕上显示的全部数据(例如,传递一个用户 ID 而不是用户对象)。也要避免传递被多个屏幕使用的数据,这种数据应该在一个全局存储中。
你也可以把路由对象看成是一个 URL。如果你的屏幕有一个 URL,那么 URL 中应该有什么?参数不应该包含那些你认为不应该出现在 URL 中的数据。这通常意味着你应该尽可能少地保留确定屏幕是什么所需的数据。想想访问一个购物网站,当你看到产品列表时,URL 通常包含类别名称、排序类型、过滤器等,它不包含屏幕上显示的实际产品列表。
例如,如果你有一个简介屏幕。当导航到它时,你可能会想在参数中传递用户对象:
// Don't do this
navigation.navigate('Profile', {
user: {
id: 'jane',
firstName: 'Jane',
lastName: 'Done',
age: 25,
},
})
这看起来很方便,让你用 route.params.user
访问用户对象,而不需要任何额外的工作。
然而,这是个反模式的做法。像用户对象这样的数据应该在你的全局存储中,而不是在导航状态中。否则,你就会在多个地方重复相同的数据。这可能会导致一些错误,比如即使用户对象在导航后发生了变化,配置文件屏幕也会显示过时的数据。
通过 DeepLink 或在网络上链接到屏幕也成为问题,因为:
- URL 是屏幕的代表,所以它也需要包含参数,即完整的用户对象,这可能会使 URL 变得非常长且不可读
- 由于用户对象在 URL 中,所以有可能传递一个随机的用户对象,代表一个不存在的用户,或者在配置文件中的数据不正确的用户
- 如果没有传递用户对象,或者格式不正确,这可能会导致崩溃,因为屏幕不知道如何处理它。
一个更好的方法是在 params
中只传递用户的 ID:
navigation.navigate('Profile', { userId: 'jane' })
实质上,在 params
中传递识别屏幕所需的最少的数据,在很多情况下,这仅仅意味着传递一个对象的 ID 而不是传递一个完整的对象。将你的应用程序数据与导航状态分开。
5. 配置 Header
5.1 配置 title
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} options={{ title: 'My home' }} />
</Stack.Navigator>
)
}
5.2 在 title 中使用 params
为了在标题中使用 params
,我们需要让屏幕的 options
成为一个返回配置对象的函数。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} options={{ title: 'My home' }} />
<Stack.Screen name='Profile' component={ProfileScreen} options={({ route }) => ({ title: route.params.name })} />
</Stack.Navigator>
)
}
试图在 options 里面使用 this.props
可能是很诱人的,但是因为它是在组件渲染之前定义的,这并不是指组件的一个实例,因此没有 props
可用。相反,如果我们把 options 变成一个函数,那么 React Navigation 会用一个包含 { navigation, route }
的对象来调用它。- 在这种情况下,我们所关心的是路由,它是作为路由 props 传递给你的屏幕 props 的同一个对象。你可能记得,我们可以通过 route.params
获得参数
传入 options
函数的参数是一个具有以下属性的对象:
navigation
route
5.3 使用 setOptions
更新 options
/* Inside of render() of React class */
<Button title='Update the title' onPress={() => navigation.setOptions({ title: 'Updated!' })} />
5.4 调整 header 样式
3 个属性调整 header 样式:
headerStyle
:一个样式对象,它将被应用到包裹 Header 的View
中。headerTintColor
:后退按钮和标题都使用这个属性作为其颜色。headerTitleStyle
:如果我们想为标题定制 fontFamily、fontWeight 和其他文本样式属性,我们可以用这个来做。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name='Home'
component={HomeScreen}
options={{
title: 'My home',
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>
</Stack.Navigator>
)
}
这里有几件事情需要注意:
- 在 iOS 上,状态栏的文字和图标都是黑色的,这在深色的背景上看起来并不好看。我们不会在这里讨论这个问题,但你应该确保按照状态栏指南中的描述,将状态栏配置为适合你的屏幕颜色。
- 我们设置的配置只适用于主屏幕;当我们导航到细节屏幕时,就恢复了默认的样式。我们现在来看看如何在屏幕之间共享选项。
5.5 在屏幕间共享 options
将配置移到 Stack.Navigator 的 screenOptions
属性下 。
function StackScreen() {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}>
<Stack.Screen name='Home' component={HomeScreen} options={{ title: 'My home' }} />
</Stack.Navigator>
)
}
5.6 覆盖 title 组件
function LogoTitle() {
return <Image style={{ width: 50, height: 50 }} source={require('@expo/snack-static/react-native-logo.png')} />
}
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} options={{ headerTitle: props => <LogoTitle {...props} /> }} />
</Stack.Navigator>
)
}
你可能想知道,当我们提供一个组件而不是像以前那样提供 title,为什么是
headerTitle
?原因是headerTitle
是一个特定于 Stack.Navigator 的属性,headerTitle
默认为一个显示 title 的Text
组件。
完整列表
5.7 添加一个 button
与 Header 互动的最常见方式是点击标题左边或右边的按钮。让我们在标题的右侧添加一个按钮(在整个屏幕上最难触摸的地方之一,取决于手指和手机的大小,但也是一个正常的放置按钮的地方)。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name='Home'
component={HomeScreen}
options={{
headerTitle: props => <LogoTitle {...props} />,
headerRight: () => <Button onPress={() => alert('This is a button!')} title='Info' color='#fff' />,
}}
/>
</Stack.Navigator>
)
}
当我们这样定义我们的按钮时,options
中的 this
变量不是 HomeScreen
实例,所以你不能对它调用 setState
或任何实例方法。这一点非常重要,因为希望你的 Header 中的按钮与 Header 所属的屏幕进行交互是非常常见的。所以,我们接下来会看看如何做到这一点。
5.8 Header 与其屏幕组件的互动
function HomeScreen({ navigation }) {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
navigation.setOptions({
headerRight: () => <Button onPress={() => setCount(c => c + 1)} title='Update count' />,
})
}, [navigation])
return <Text>Count: {count}</Text>
}
在这里,我们用一个带有 onPress
处理程序的按钮来更新 headerRight
,这个处理程序可以访问组件的状态,并可以更新它。
5.9 自定义返回 button
createNativeStackNavigator
为返回按钮提供了特定平台的默认值。在 iOS 上,这包括按钮旁边的一个标签,当标题符合可用空间时,它显示前一个屏幕的标题,否则它显示 "返回"。
你可以用 headerBackTitle
改变标签行为,用 headerBackTitleStyle
改变其样式(阅读更多)。
要自定义返回按钮的图像,你可以使用 headerBackImageSource
(阅读更多)。
5.10 覆盖返回 button
只要用户有可能从当前屏幕返回,后退按钮就会在 Stack.Navigator 中自动呈现--换句话说,只要堆栈中存在多个屏幕,后退按钮就会呈现。
一般来说,这是你想要的。但在某些情况下,你有可能想通过上面提到的选项对后退按钮进行更多的定制,在这种情况下,你可以将 headerLeft
选项设置为一个将被渲染的 React 元素,就像我们对 headerRight
所做的一样。另外,headerLeft
选项也可以接受一个 React 组件,例如,它可以用来覆盖后退按钮的 onPress
行为。在api 参考中阅读更多关于这个的内容。
6. 嵌套导航
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name='Feed' component={Feed} />
<Tab.Screen name='Messages' component={Messages} />
</Tab.Navigator>
)
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name='Home' component={Home} options={{ headerShown: false }} />
<Stack.Screen name='Profile' component={Profile} />
<Stack.Screen name='Settings' component={Settings} />
</Stack.Navigator>
</NavigationContainer>
)
}
上边的代码结构
Stack.Navigator
Home
(Tab.Navigator)Feed
(Screen)Messages
(Screen)
Profile
(Screen)Settings
(Screen)
6.1 嵌套导航器注意事项
当使用嵌套导航器时,有一些事情需要记住:
-
每个导航器都保留自己的导航历史
例如,当你在一个嵌套 Stack.Navigator 的屏幕内按下后退按钮时,它会回到嵌套堆栈内的前一个屏幕,即使有另一个导航器作为父级。
-
每个导航器都有自己的 options
例如,在一个嵌套在子导航器中的屏幕中指定一个标题选项,不会影响在父导航器中显示的标题。
如果你想实现这种行为,请看嵌套导航器的屏幕选项指南。如果你在一个 Stack.Navigator 中渲染一个 TabNavigator ,并想在 StackNavigator 的 Header 中显示标签导航器中的活动屏幕的标题,这可能很有用。
-
一个导航器中的每个屏幕都有自己的参数
例如,传递给嵌套导航器中的屏幕的任何参数都在该屏幕的路由 props 中,不能从父级或子级导航器的屏幕中访问。
如果你需要从子屏幕上访问父屏幕的参数,你可以使用
React Context
将参数暴露给子屏幕。 -
导航操作由当前导航器处理,如果无法处理则会冒泡上升。
例如,在嵌套的屏幕中调用
navigation.goBack()
,如果您已经在导航器的第一个屏幕上,则仅会在父导航器中返回。其他操作(如navigate
)也类似,即在嵌套导航器中进行导航,如果嵌套导航器无法处理,则父导航器将尝试处理。在上面的示例中,当在 Feed 屏幕中调用
navigate('Messages')
时,嵌套的Tab.Navigator
将处理它,但如果调用navigate('Settings')
,则父Stack.Navigator
将处理它。 -
嵌套的导航器中可用的导航器特定方法
例如,如果在 DrawerNavigator 中有一个 StackNavigator,那么在屏幕中的
navigation
prop 上也将可用 Drawer.Navigator 的openDrawer
、closeDrawer
、toggleDrawer
等方法。但是,如果您有一个 StackNavigator 作为 DrawerNavigator 的父级,则 StackNavigator 内的屏幕将无法访问这些方法,因为它们没有嵌套在 DrawerNavigator 中。同样,如果您在 StackNavigator 内有一个 TabNavigator,则 TabNavigator 中的屏幕将在其
navigation
prop 中获取 StackNavigator 的push
和replace
方法。如果您需要从父级向嵌套的子导航器发送操作,则可以使用
navigation.dispatch
:
navigation.dispatch(DrawerActions.toggleDrawer())
-
嵌套的导航器不接收父级的事件
例如,如果你有一个嵌套在 TabNavigator 内的 StackNavigator,当使用
navigation.addListener
时,StackNavigator 中的屏幕不会收到父 TabNavigator 发出的事件,例如(tabPress)。为了接收来自父级导航器的事件,你可以用
navigation.getParent
明确地监听父级的事件:
const unsubscribe = navigation
.getParent('MyTabs')
.addListener('tabPress', (e) => {
// Do something
});
这里 'MyTabs'
指的是你在父级 Tab.Navigator 的id props 中传递的值,你想监听的是这个事件。
-
父级导航器的 UI 渲染在子级导航器之上
例如,当您在 Drawer.Navigator 中嵌套一个 Stack.Navigator 时,您会发现 Drawer.Navigator 出现在 Stack.Navigator 的标题栏之上。然而,如果您将 Drawer.Navigator 嵌套在 Stack.Navigator 中,则 Drawer.Navigator 将出现在堆栈的标题栏之下。在决定如何嵌套您的导航器时,这是一个重要的考虑点。
在您的应用程序中,您可能会根据所需的行为使用以下模式:
- 在 Stack.Navigator 的初始屏幕中嵌套 Tab.Navigator - 新屏幕在推送时覆盖选项卡栏。
- 在 Stack.Navigator 的初始屏幕中嵌套 Drawer.Navigator ,并隐藏初始屏幕的堆栈标题 - 抽屉只能从堆栈的第一个屏幕打开。
- 在 Drawer.Navigator 的每个屏幕中嵌套 Stack.Navigator - 抽屉从堆栈的标题栏上方出现。
- 在 Tab.Navigator 的每个屏幕中嵌套 Stack.Navigator - 选项卡栏始终可见。通常再次按选项卡会将堆栈弹回到顶部。
6.2 在一个嵌套的导航器中导航到一个屏幕
function Root() {
return (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Drawer.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Root"
component={Root}
options={{ headerShown: false }}
/>
<Stack.Screen name="Feed" component={Feed} />
</Stack.Navigator>
</NavigationContainer>
);
}
在这里,你可能想从你的 Feed 组件导航到
Root 屏幕:
navigation.navigate('Root');
它可以工作,并且显示 Root 组件内的初始屏幕,也就是主页。但有时你可能想控制导航时应显示的屏幕。为了实现这一点,你可以在 params
中传递屏幕的名称:
navigation.navigate('Root', { screen: 'Profile' });
现在,导航时将呈现 Profile
屏幕而不是 Home
。
这可能看起来与之前导航在嵌套屏幕上的工作方式有很大不同。不同的是,在以前的版本中,所有的配置都是静态的,所以 `React Navigation 可以通过遍历嵌套配置来静态地找到所有导航器及其屏幕的列表。但在动态配置下,React Navigation在包含屏幕的导航器渲染之前,并不知道哪些屏幕是可用的,在哪里。通常情况下,一个屏幕在你导航到它之前不会渲染其内容,所以还没有渲染的导航器的配置还不可用。这使得你有必要指定你要导航的层次结构。这也是为什么你应该尽可能地减少导航器的嵌套,以保持你的代码更简单。
6.3 在嵌套导航器中向屏幕传递参数
navigation.navigate('Root', {
screen: 'Profile',
params: { user: 'jane' },
});
如果导航器已经被渲染了,在 StackNavigator 的情况下,导航到另一个屏幕将推送一个新的屏幕。
对于深度嵌套的屏幕,你可以遵循类似的方法。注意,这里导航的第二个参数只是 params
,所以你可以做这样的事情:
navigation.navigate('Root', {
screen: 'Settings',
params: {
screen: 'Sound',
params: {
screen: 'Media',
},
},
});
在上面的例子中,你正在导航到 Media
屏幕,它在一个导航器中,嵌套在 Sound
屏幕中,而 Sound
在一个导航器中,嵌套在 Settings
屏幕中。
6.4 嵌套多个导航器
嵌套多个导航器,如 stack、drawer 或 tabs,有时是很有用的。
当嵌套多个stack、drawer 或底部tabs 导航器时,子导航器和父导航器的标题都会显示。然而,通常情况下,在子导航器中显示标题,而在父导航器的屏幕中隐藏标题是更可取的。
为了实现这一点,你可以使用 headerShown: false
选项在包含导航器的屏幕中隐藏 Header。
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name="Profile" component={Profile} />
<Tab.Screen name="Settings" component={Settings} />
</Tab.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
<Stack.Screen name="EditPost" component={EditPost} />
</Stack.Navigator>
</NavigationContainer>
);
}
在这些例子中,我们使用了一个直接嵌套在另一个 StackNavigator 内的底部 TabNavigator,但是当中间有其他导航器时,同样的原则也适用,例如:StackNavigator 在一个 TabNavigator内,而这个 TabNavigator在另一个 StackNavigator内,StackNavigator在 DrawerNavigator内等等。
如果你不希望任何一个导航器中出现标题,你可以在所有的导航器中指定 headerShown: false
:
function Home() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Profile" component={Profile} />
<Tab.Screen name="Settings" component={Settings} />
</Tab.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="EditPost" component={EditPost} />
</Stack.Navigator>
</NavigationContainer>
);
}
6.5 嵌套导航的最佳实践
我们建议尽量减少导航器的嵌套。尽可能少地嵌套导航器来实现您想要的行为。嵌套有许多缺点:
- 它会导致深度嵌套的视图层次结构,可能会在低端设备上引起内存和性能问题。
- 嵌套相同类型的导航器(例如选项卡内嵌选项卡,抽屉内嵌抽屉等)可能会导致令人困惑的用户体验。
- 如果嵌套过度,当导航到嵌套屏幕、配置深度链接等时,代码会变得难以跟踪。
将导航器嵌套视为实现所需 UI 的一种方式,而不是组织代码的一种方式。如果您想要创建一个单独的屏幕组进行组织,而不是使用单独的导航器,可以使用 Group 组件。
<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>
7. 导航生命周期
一个问题:当我们从 Home
导航到其他页面时,或者当我们回到 Home
页面时,Home
页面会发生什么?一个路由如何知道用户正在离开它或回到它?
如果您是从 Web 背景转入 react-navigation,您可能会认为当用户从路由 A 导航到路由 B 时,A 将卸载(其 componentWillUnmount
被调用),并且当用户回到 A 时 A 将重新挂载。虽然这些 React 生命周期方法仍然有效并且在 react-navigation 中使用,但它们的用法与 Web 不同。这是由移动导航的更复杂需求驱动的。
示例场景
考虑一个包含屏幕 A 和 B 的堆栈式导航器。导航到 A 后,将调用其 componentDidMount
。当推入 B 时,也会调用其 componentDidMount,但是 A 仍然保留在堆栈上,因此不会调用其 componentWillUnmount
。
当从 B 返回到 A 时,会调用 B 的 componentWillUnmount
,但是不会调用 A 的 componentDidMount
,因为 A 在整个过程中一直保持挂载状态。
其他导航器也可以观察到类似的结果。考虑一个包含两个选项卡的选项卡式导航器,每个选项卡都是一个堆栈式导航器:
function App() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="First">
{() => (
<SettingsStack.Navigator>
<SettingsStack.Screen
name="Settings"
component={SettingsScreen}
/>
<SettingsStack.Screen name="Profile" component={ProfileScreen} />
</SettingsStack.Navigator>
)}
</Tab.Screen>
<Tab.Screen name="Second">
{() => (
<HomeStack.Navigator>
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Details" component={DetailsScreen} />
</HomeStack.Navigator>
)}
</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
);
}
我们从 HomeScreen
开始,导航到 DetailsScreen
。然后,我们使用选项卡栏切换到 SettingsScreen
并导航到 ProfileScreen
。完成这个操作序列后,这 4 个屏幕都被挂载了!如果您使用选项卡栏切换回 HomeStack
,您会注意到您会看到 DetailsScreen
- HomeStack 的导航状态已被保留!
7.1 React Navigation 生命周期事件
现在我们了解了 React 生命周期方法在 React Navigation 中的工作方式,让我们回答我们一开始提出的问题:“我们如何知道用户何时离开(失焦)或返回(聚焦)屏幕?”
React Navigation 向订阅它们的屏幕组件发出事件。我们可以监听 focus 和 blur 事件,以分别了解屏幕何时进入焦点或离开焦点。
示例:
function Profile({ navigation }) {
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// Screen was focused
// Do something
});
return unsubscribe;
}, [navigation]);
return <ProfileContent />;
}
有关可用事件和 API 使用的更多详细信息,请参见 Navigation 事件。
我们可以使用 useFocusEffect
钩子来执行副作用,而不是手动添加事件侦听器。它类似于 React 的 useEffect
钩子,但它与导航生命周期相关联。
示例:
import { useFocusEffect } from '@react-navigation/native';
function Profile() {
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, [])
);
return <ProfileContent />;
}
如果您想基于屏幕是否聚焦来呈现不同的内容,则可以使用 useIsFocused
钩子,该钩子返回一个布尔值,指示屏幕是否聚焦。
转载自:https://juejin.cn/post/7244075109442601015