likes
comments
collection
share

react navigation7.x 中文文档青春极速mini版-Fundamentals

作者站长头像
站长
· 阅读数 45

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 将向路由器发送一个 action
    • navigategoBack 等可用于方便地调度操作

    导航器也可以接受 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.0
  • expo >= 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 函数返回一个包含 ScreenNavigator 属性的对象。它们都是用于配置导航器的 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

react navigation7.x 中文文档青春极速mini版-Fundamentals

导航条和内容区的样式是 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 指定了初始路由

现在我们有两个路由: HomeDetail, 路由可以通过 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 传递额外的属性

两种方法

  1. 使用 React Context
  2. 使用 Render Props
<Stack.Screen name='Home'>{props => <HomeScreen {...props} extraData={someData} />}</Stack.Screen>

注意:默认情况下,React Navigation 会对屏幕组件进行优化,以防止不必要的渲染。使用 Render Props 会移除这些优化。因此,如果你使用它,你需要确保你的 Screen 组件使用 React.memoReact.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')} />

navigatepush: 它们的区别在于

  • 如果已经加载了页面,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. 传递参数

传递参数需要两个步骤:

  1. 作为 navigate 函数的第二个参数 navigation.navigate('RouteName', { /* params */})
  2. 在你的 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>
  )
}

react navigation7.x 中文文档青春极速mini版-Fundamentals

这里有几件事情需要注意:

  • 在 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 的 openDrawercloseDrawertoggleDrawer 等方法。但是,如果您有一个 StackNavigator 作为 DrawerNavigator 的父级,则 StackNavigator 内的屏幕将无法访问这些方法,因为它们没有嵌套在 DrawerNavigator 中。

    同样,如果您在 StackNavigator 内有一个 TabNavigator,则 TabNavigator 中的屏幕将在其 navigation prop 中获取 StackNavigator 的 pushreplace 方法。

    如果您需要从父级向嵌套的子导航器发送操作,则可以使用 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 将出现在堆栈的标题栏之下。在决定如何嵌套您的导航器时,这是一个重要的考虑点。

    在您的应用程序中,您可能会根据所需的行为使用以下模式:

    1. 在 Stack.Navigator 的初始屏幕中嵌套 Tab.Navigator - 新屏幕在推送时覆盖选项卡栏。
    2. 在 Stack.Navigator 的初始屏幕中嵌套 Drawer.Navigator ,并隐藏初始屏幕的堆栈标题 - 抽屉只能从堆栈的第一个屏幕打开。
    3. 在 Drawer.Navigator 的每个屏幕中嵌套 Stack.Navigator - 抽屉从堆栈的标题栏上方出现。
    4. 在 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
评论
请登录