RN路由-React Navigation组件5.x-基本原理(中文文档)

7,573 阅读48分钟

##引言 React Native路由导航,有它就够了!该文档根据React Navigation文档翻译,有些内容会根据自己的理解进行说明,不会照搬直译,若发现理解有问题的地方,欢迎大家提点!由于本人是基于iOS开发,安卓版本的目前还没有去实践运行,后续有时间会去实践,如果遇到问题,可以@我。最后,这边针对iOS运行的时候遇到的问题也有汇总,并提供解决方案。最后的最后,由于本片文章会很长,所以推荐一个Chrome插件,可以自动根据文章中的h1~h6生成目录,方便查看章节内容,在编写文章时也可以用哦!Smart TOC,点击安装后,如下图操作:

截屏2020-06-0810.27.10.png

基本原理

1 开始

如果您已经熟悉React Native,那么您将能够快速上手React导航!如果没有学习过,你需要先读React Native Express的第1 - 4部分(包括第4部分),读完后再回到这里。

本文档的基础部分介绍React导航的最重要的方面。它足以让您了解如何构建典型的小型移动应用程序,并为您提供深入了解React导航更高级部分所需的背景知识。

1.1 安装

在RN项目中安装您需要的包

  • npm
npm install @react-navigation/native
  • yarn
yarn add @react-navigation/native

React导航由一些核心工具组成,并且导航器使用这些工具在应用中创建导航结构。为了提前加载安装工作,我们还需要安装和配置大多数导航器使用的依赖项,然后我们开始编写代码。

现在,我们需要安装react-native-gesture-handler、 react-native-reanimatedreact-native-screens 、 react-native-safe-area-context、 @react-native-community/masked-view这些库,如果您已经安装了这些最新版本的库,那可以跳过下面内容,否则继续阅读下去。

1.2 安装依赖到Expo管理项目

cd到你的项目目录下,运行:

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

这个命令会安装这些库的最合适的版本。接下来,您可以继续到项目中编写代码。 (注:用Expo管理项目,目前还没用到过,有疑问的童鞋麻烦自行查询!)

1.3 安装依赖到原生的RN项目中

cd到你的项目目录下,运行:

  • npm
npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
  • yarn
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

注意:安装后可能会出现有相关的对等依赖的警告,这个通常是由于一些不同版本的包引起的,只要您的项目可以顺利运行,可以忽略这些警告。

从RN 0.60或者更高版本开始,这些库会自动链接项目,因此不需要运行react-native link。

如果您是在mac上开发iOS项目,需要安装CocoaPods来完成项目链接:

npx pod-install ios

当您完成了react-native-gesture-handler的安装,在项目的入口文件(例如index.js或App.js)引入react-native-gesture-handler(确保在入口文件的第一行)

import 'react-native-gesture-handler';

注意:如果您忽略这一步,尽管在开发中运行是正常的,但在生产上将会奔溃。

现在,我们需要将整个app装载在NavigationContainer之中。通常做法是,在入口文件(例如index.js或App.js)做这些事情:

import 'react-native-gesture-handler';
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
  );
}

注意:当您使用导航器(如堆栈导航器)时,对于任何其他依赖项,都需要遵循导航器的安装说明。如果您遇到"Unable to resolve module"的错误,您需要安装错误中提示的组件到项目中。

现在,您可以将项目编译并运行在设备或者模拟器上,并继续进行相关的代码编写。

1.4 遇到的问题

报错:TypeError: null is not an object (evaluating '_RNGestureHandlerModule.default.Direction')

解决方法: 在ios文件夹下的Podfile文件中添加:

pod 'RNGestureHandler', :path => "../node_modules/react-native-gesture-handler"

终端命令cd到ios文件,运行pod install

2 Hello React Navigation

在web浏览器上,您可能会用一个标签链接到另一个页面上。当用户点击了一个链接,URL被推到浏览器历史堆栈中,当用户点击了返回按钮,浏览器会从历史堆栈中弹出之前的访问过的页面作为目前展示的页面。React Native不像web浏览器那样有内置的全局历史堆栈概念——这就是React导航的作用。

React Navigation的堆栈导航器为应用程序提供了在屏幕之间转换和管理导航历史的方法。如果您的应用程序只使用一个堆栈导航器,那么它在概念上类似于web浏览器处理导航状态——当用户与它交互时,您的应用程序从导航堆栈中推送和弹出项目,用户可以看到不同的页面。它在web浏览器和React导航中的工作方式的一个关键区别是,React导航的堆栈导航器提供了在Android和iOS中导航堆栈中的路由时需要的手势和动画。

让我们从演示最常见的导航器开始,createStackNavigator。

2.1 安装堆栈导航器库

到目前为止,我们安装是导航器的构建块和共享基础的库,React Navigation中的每个导航器都在自己的库中。要使用堆栈导航器,我们需要安装@ response -navigation/stack:

  • npm
npm install @react-navigation/stack
  • yarn
yarn add @react-navigation/stack

提醒:@react-navigation/stack 依赖于我们在开始章节安装的库 @react-native-community/masked-view,如果您还未安装,麻烦回到上一章节。

2.2 创建一个堆栈导航器

createStackNavigator是一个函数,它返回一个包含两个属性的对象:屏幕和导航器。它们都是用于配置导航器的React组件。导航器应该将屏幕元素作为子元素来定义路由的配置。

NavigationContainer是一个管理导航树并包含导航状态的组件。该组件必须包装所有导航器结构。通常,我们会将这个组件呈现在应用程序的根目录下,这个根目录通常是从app .js导出的组件。这里我自定义了一个路由文件,然后在app.js中引入:

//自定义一个路由文件NavigationComponent.js
import React from 'react';
import {Text, View} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

function HomeScreen({navigation}) {
  return (
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Home Screen</Text>
    </View>
  );
}

const Stack = createStackNavigator();

function NavigationComponent() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default NavigationComponent;
//在App.js中引入
import 'react-native-gesture-handler';
import React, {Component} from 'react';
import NavigationComponent from './Sections/常用组件/NavigationComponent';

export default class App extends Component {
  render() {
    return <NavigationComponent />;
  }
}

在Snack上尝试编写

Simulator Screen Shot - iPhone 11 - 2020-06-08 at 15.03.00.png
如果您运行这段代码,您将看到一个带有空导航栏和包含主屏幕组件的灰色内容区域的屏幕(如上所示)。您看到的导航栏和内容区域的样式是堆栈导航器的默认配置,稍后我们将学习如何配置它们。

路由名称的对大小写不敏感——您可以使用小写的home或大写的home,这取决于您。我们喜欢把路线名称大写。

屏幕唯一需要的配置是name和component props。您可以在stack navigator reference中了解更多其他可用选项的信息。

2.3 配置导航器

所有路由配置都被指定为导航器的props。我们没有向导航器传递任何props,因此它只使用默认配置。

让我们在堆栈导航器中添加第二个屏幕,并配置主屏幕首先渲染:

function HomeScreen() {...}

function DetailsScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
    </View>
  );
}

const Stack = createStackNavigator();

function NavigationComponent() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

在Snack上尝试编写

现在我们的堆栈有两个路由页面,一个主页面和一个详细页面。可以使用Screen组件指定路由。屏幕组件接受一个name prop,对应于导航的路由的名称,以及一个component prop,对应于它将渲染的组件。

这里,主路由对应于HomeScreen组件,而详细信息路由对应于DetailsScreen组件。堆栈的初始路由是主路由。尝试将其更改为Details并重新加载应用程序(如您所料,React Native的快速刷新不会更新initialRouteName的更改),注意您现在将看到Details屏幕。然后将其更改为Home并重新加载一次。

注意:组件prop只接受component,而不是渲染函数。不要传递内联函数(例如component={() => }),否则当父组件重新渲染时,你的组件将会卸载和重新加载并移除所有的state。见Passing additional props来替代。

2.4 指定options

在导航器里的每个屏幕可以指定一些options,例如一个渲染页面的标题。这些options可以传递到每个屏幕组件的options prop中:

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{ title: 'Overview' }}
/>

在Snack上尝试编写

有时我们希望为导航器中的所有屏幕指定相同的options。为此,我们可以向导航器传递screenOptions prop。

2.5 传递props

有时我们可能想要传递props到屏幕上。我们可以用两种方法来实现:

1.使用React context提供程序包装导航器,以便向屏幕传递数据(推荐)。

2.使用屏幕渲染回调来代替指定一个组件的prop:

<Stack.Screen name="Home">
  {props => <HomeScreen {...props} extraData={someData} />}
</Stack.Screen>

注意:默认情况下,React Navigation会对屏幕组件进行优化,以防止不必要的渲染。使用渲染回调会移除这些优化。所以如果你使用渲染回调,需要为组件使用React.memo或React.PureComponent,以避免性能问题。

2.6 下一步?

接下来的问题是:“我如何从主页面跳转到详情页面?”,这将在下一节中介绍。

2.7 总结

  • React Native没有像web浏览器那样的内置导航API。React导航为你提供了这个功能,以及提供iOS和Android的手势和动画来在屏幕之间切换。
  • Stack.Navigator是一个组件,它将路由配置为子组件和用于配置props,并渲染内容。
  • 每个Stack.Screen组件接受一个名称prop,该名称引用路由的名称,而component prop被指定为路由渲染的组件。这是2个必需的props。
  • 要指定堆栈中的初始路由是什么,请设置initialRouteName作为导航器的prop。
  • 要指定特定于屏幕的options,我们可以向堆栈传递一个options prop。对于常见的options,我们可以将屏幕options传递给Stack.Navigator。

3 屏幕页面切换

在前面的章节,我们定义了一个包含主页面和详情页面的堆栈路由,但是我们没有学习如何让用户导航主页面和详细页面(虽然我们学习了在代码中如何改变初始路由,但让用户强制克隆库和改变初始路由,以达到展示另一个页面,可以想象,这是最糟糕的用户体验)。

如果是web浏览器,我们可以这样:

<a href="details.html">Go to Details</a>

另一个方法是:

<a
  onClick={() => {
    window.location.href = 'details.html';
  }}
>
  Go to Details
</a>

我们将执行与全局window.location类似的操作,我们将使用navigation prop向下传递到屏幕组件。

3.1 导航到新页面

import React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

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>
  );
}

// ... other code from the previous section

在Snack上尝试编写

让我们来分析一下:

  • navigation - 在堆栈导航器中,navigation这个属性被传递到每个屏幕组件(定义)中。稍后将在"深入导航属性"中详细介绍 。
  • navigate('Details') - 我们用路由的名称来调用导航函数来达到用户想要看到的页面(在导航属性上——命名是困难的!)。

如果我们使用navigation.navigate,导航到未在堆栈导航器上定义的路由名称,将不会发生任何事情。换句话说,我们只能导航到在堆栈导航器上定义的路由——不能导航到任意组件。 现在在我们的栈上有两个路由:(1)主路由,(2)详情路由。如果我们在详情页面再次导航到详情页面,会发生什么?

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>
  );
}

在Snack上尝试编写

如果你运行这段代码,当你点击"Go to Details... again"后,不会做任何事情!因为我们已经在详情页面上了。

假设,我们确实想跳转到一个新的详情页面。这在向每个路由传递一些唯一数据的情况下非常常见(稍后在讨论参数时将对此进行更多讨论!)。我们可以用navigate的push来达到目的,这个允许添加一个路由而不必理会栈上是否有这个路由。

<Button
  title="Go to Details... again"
  onPress={() => navigation.push('Details')}
/>

在Snack上尝试编写 

每次使用push,在栈导航器上都会新增一个路由。而当使用navigate时,首先会判断是否有这个名称的路由,当栈上没有这个路由时才会跳转到一个新的路由页面。

3.2 返回

在路由中,当前页面的头部会有一个返回按钮,点击返回按钮可以回到之前的页面(但如果在路由中只有一个页面,头部没有返回按钮,也无法操作返回按钮)。

有时候你想通过编写代码来完成这个操作,我们使用navigation.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>
  );
}

在Snack上尝试编写

在Android上,React Navigation在用户点击硬件返回按钮时,返回事件是一样的。 另一个常见的需求是多个页面的返回--例如,你已经跳转了多个页面,想直接回到第一个页面,在本例中,我们要返回Home,所以我们可以使用navigate('Home')(而不是push!试试看,看看有什么不同)。另一种选择是navigation.popToTop(),它返回到堆栈中的第一个屏幕。

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()} />
      <Button
        title="Go back to first screen in stack"
        onPress={() => navigation.popToTop()}
      />
    </View>
  );
}

在Snack上尝试编写

3.3 总结

  • navigation.navigate('RouteName')如果新路由不在堆栈中,则将其推到堆栈导航器,否则将跳转到这个页面。
  • navigation.push('RouteName')可以多次跳转同一个路由页面。
  • header bar会自带一个返回按钮提供返回操作,但是我们可以使用navigation.goBack()来实现返回操作。在Android上的硬件返回按钮的返回操作是一样的效果。
  • 你能使用navigation.navigate('RouteName')返回到一个已经存在的页面,并且你可以使用navigation.popToTop()返回第一个页面。
  • 在所有的页面组件中都可以获取到navigation属性(只要组件被定义为路由配置和使用React Navigation来渲染路由)。

4 路由间传递参数

还记得我说过”稍后在讨论参数时将对此进行更多讨论!“吗?现在可以开始了。

现在我们知道如何在栈导航器上创建一些路由,并且路由之间的跳转,那么如何在路由之间传递参数呢,我们来看看。

这里有两部分:

  1. 将路由上需要的参数放在一个对象里,作为navigation.navigate函数的第二个参数:navigation.navigate('RouteName', { /* params go here */ })
  2. 在组件中获取这个参数:route.params

我们推荐传递的参数是JSON格式。这样,您就能够使用状态持久性 ,并且您的屏幕组件可以使用正确的约定来实现深度链接

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => {
          /* 1. Navigate to the Details route with params */
          navigation.navigate('Details', {
            itemId: 86,
            otherParam: 'anything you want here',
          });
        }}
      />
    </View>
  );
}

function DetailsScreen({ route, navigation }) {
  /* 2. Get the param */
  const { itemId } = route.params;
  const { otherParam } = route.params;
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Text>itemId: {JSON.stringify(itemId)}</Text>
      <Text>otherParam: {JSON.stringify(otherParam)}</Text>
      <Button
        title="Go to Details... again"
        onPress={() =>
          navigation.push('Details', {
            itemId: Math.floor(Math.random() * 100),
          })
        }
      />
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
      <Button title="Go back" onPress={() => navigation.goBack()} />
    </View>
  );
}

在Snack上尝试编写

4.1 更新参数

页面上也可以更新参数,类似更新页面状态。navigation.setParams就可以用来更新页面参数。通过API reference for了解更多。

你也可以向页面传递一些初始参数。如果导航到页面并没有设置任何参数,这个初始参数将会被使用。它们会与传递的参数进行浅合并。初始参数被指定为initialParams 属性:

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  initialParams={{ itemId: 42 }}
/>

4.2 传递参数到之前的页面

不仅仅能传递参数到新的页面,也能传递参数到之前的页面。例如,在页面上创建一个post按钮,并且点击这个按钮创建一个新的post页面。在创建了post页面以后,你想传递一些数据到上一个页面。

想做到这个,你可以使用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]);

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Create post"
        onPress={() => navigation.navigate('CreatePost', {
            text: route.params?.post ? route.params?.post : '',
        })}
      />
      <Text style={{ margin: 10 }}>Post: {route.params?.post}</Text>
    </View>
  );
}

function CreatePostScreen({ navigation, route }) {
  //将已经输入的内容,传递过来赋值给TextInput的处理逻辑
  let {text} = route.params;
  let [postText, setPostText] = React.useState(text);

  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 params back to home screen
          navigation.navigate('Home', { post: postText });
        }}
      />
    </>
  );
}

在Snack上尝试编写 

当点击”Done“按钮,TextInput输入的内容就会回传到主页面上并刷新展示在页面上。

4.3 嵌套的导航页面传递参数

如果你有一个嵌套的导航器,传递参数有些许不同。例如,你有一个叫做Account的页面,想要传递参数到Settings页面。需要做如下操作:

navigation.navigate('Account', {
  screen: 'Settings',
  params: { user: 'jane' },
});

了解更多请点击Nesting navigators

4.4 总结

  • navigate和push在导航页面时可以作为可选的第二个参数传递参数。例如:navigation.navigate('RouteName', {paramName: 'value'})
  • 在页面上可以通过route.params读取传递的参数。
  • 可以通过navigation.setParams更新页面参数。
  • 通过initialParams属性可以传递初始化的参数。

5 配置header bar

我们已经知道怎么配置header标题,但是让我们在学习其他options之前再回顾一遍——复习是学习的关键

5.1 设置header标题

组件接受options prop,它是一个对象或返回一个对象的函数,它包含各种配置选项。其中一个就是title,看下面的例子:

function StackScreen() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{ title: 'My home' }}
      />
    </Stack.Navigator>
  );
}

在Snack上尝试"header title"

5.2 在title中使用参数

为了在title中使用参数,我们需要为页面设置一个返回配置对象的函数。在options中尝试使用this.props是很有用的,但是在组件渲染之前,没有引用组件的实例,因此没有可用的props。相反,如果我们将options设置为一个函数,那么React Navigation将用一个包含{Navigation, route}的对象调用它——在这种情况下,我们所关心的是route,它与作为route prop传递给页面prop的对象是同一对象。还记得我们可以通过route得到参数。参数,下面我们用这个来提取参数并使用它作为标题。

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>
  );
}

在Snack上尝试"params in title"

传入选项函数的参数是一个具有以下属性的对象:

在上面的例子中我们只需要route属性,在某些案例中,我们还需要用到navigation属性。

5.3 用setOptions更新options

通常需要在安装的屏幕组件本身更新活动页面的选项配置。我们可以使用navigation.setOptions来做实现。

/* Inside of render() of React class */
<Button
  title="Update the title"
  onPress={() => navigation.setOptions({ title: 'Updated!' })}
/>

在Snack上尝试"updating navigation options" 

5.4 调整header样式

自定义header样式有3个关键属性:headerStyle,headerTintColor,和 headerTitleStyle

  • headerStyle:应用于包装头部的视图的样式对象。如果你设置它的backgroundColor,就是设置header的颜色。
  • headerTintColor:back按钮和标题都使用这个属性作为字体颜色。在下面的示例中,我们将色调设置为白色(#fff),这样返回按钮和标题标题就是白色的。
  • headerTitleStyle:可以使用它来自定义fontFamily、fontWeight和其他text样式。
function NavigationComponent() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{
          title: 'My home',
          headerStyle: {
            backgroundColor: '#f4511e',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}
      />
    </Stack.Navigator>
  );
}

在Snack上尝试"header styles"

需要注意:

  1. 在iOS上,状态栏上的字体和icon是黑色的,在深色的背景上显示不是很友好,我们不会在这里讨论它,但是您应该确保配置状态栏,使其适应状态栏指南您的屏幕颜色。
  2. 我们仅仅在主页面配置了这些,当跳转到详情页面时,header又会恢复原来的样式。接下来,我们看看如何在页面之间共享样式。

5.5 页面之间共享options

通常,在多个页面上我们会设置一个相同样式的header。例如,你的公司品牌颜色是红色,然后页面header想设置为红色背景和白色字体。在上面的例子中,我们配置了主页面header的颜色,当我们跳转到详情页面时,header恢复到默认的样式了。如果我们将主页面的样式配置复制到详情页面上,那会很麻烦,如果app每个页面都需要这个样式呢?我们不用这么做,我们可以将样式配置移动到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>
  );
}

在Snack上尝试"sharing header styles"

现在,属于StackScreen上的页面都会有一个主题样式,但是,如果我们需要重载这些配置,有什么方法呢?

5.6 用自定义组件代替标题

有时候,你不仅仅只是想改变title的text和样式 -- 比如,你想用一张图片替代标题,或者将标题设置为按钮。在这些案例中,你完全可以使用自定义的组件来重载标题。

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>
  );
}

在Snack上尝试"custom header title component"

你可能注意到,为什么我们设置headerTitle为一个组件,而不是像之前一样设置title?因为headerTitle是Stack.Navigator的一个属性,headerTitle默认是一个Text组件,用来展示title的。

5.7 额外的配置

你可以在createStackNavigator reference里浏览stack navigator里所有的配置列表。

5.8 总结

  • 你可以通过options里的属性自定义页面的header样式。阅读所有的options in the API reference
  • options可以是一个对象或者一个函数。当是函数时,它提供了一个包含navigation和route属性的对象。
  • 在初始化时,可以通过screenOptions配置一个共享的样式。这个属性优先级高于配置。

6 Header buttons

现在我们知道如何去定义header的外观了,接下来的操作,可能会让我们更有动力:通过自定义配置来响应用户的触摸。

6.1 在header上添加button

大多数header上的交互是在左边或者右边有个按钮可以点击。让我们在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>
  );
}

在Snack上尝试"header button"

当我们通过这个方法定义按钮,由于它不是HomeScreen实例的变量选项,因此不能使用setState或者其他实例方法去改变它。这是非常重要的,因为在header上添加button交互是非常常见的。因此,我们看看接下来怎么做。

6.2 Header与页面组件的交互

为了能够与页面组件交互,我们使用navigation.setOptions代替options属性来定义button。在页面组件中使用navigation.setOptions,我们可以访问页面的props,state,context等。

function StackScreen() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={({ navigation, route }) => ({
          headerTitle: props => <LogoTitle {...props} />,
        })}
      />
    </Stack.Navigator>
  );
}

function HomeScreen({ navigation }) {
  const [count, setCount] = React.useState(0);

  React.useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <Button title="Update count!" onPress={() => setCount((c) => c + 1)} />
      ),
    });
  }, [navigation, setCount]);

  return <Text>Count: {count}</Text>;
}

在Snack上尝试"header interaction"  

6.3 自定义返回按钮

createStackNavigator提供了特定平台的默认的返回按钮,在iOS上包含button和text,在可用空间中,text展示的是上个页面的标题,否则只展示Back内容。

你可以通过headerBackTitle和headerTruncatedBackTitle来修改label的行为(更多)。

你可以使用headerBackImage自定义返回按钮的图片。

6.4 重写返回按钮

只要用户可以从当前页面返回之前的页面,返回按钮就会自动被渲染到栈导航器上--换句话说,只要在栈上有超过一个页面,返回按钮就会自动被渲染。

一般来说,这就是你想要的。但在某些情况下,您可能更希望定制back按钮,而不是通过上面提到的选项,在这种情况下,您可以将headerLeft选项设置为将要呈现的React元素,就像我们对headerRight所做的那样。另外,headerLeft选项还接受一个React组件,例如,可以使用该组件重写back按钮的onPress行为。更多相关内容请参阅api reference

6.5 总结

  • 你可以用options里的headerLeft和headerRight属性,在header上设置buttons。
  • 返回按钮可以完全使用headerLeft自定义,但假如你仅仅只是想修改title和image,有其他的属性可以设置--headerBackTitle、headerTruncatedBackTitle,和headerBackImage。

7 嵌套的导航

嵌套的导航意思是在一个导航上的页面里渲染另一个导航,例如:

首先做下前期工作,安装需要的组件库,这里需要用到@react-navigation/bottom-tabs

  • npm
npm install @react-navigation/bottom-tabs
  • yarn
yarn add @react-navigation/bottom-tabs

然后开始我们的编码:

/* 自定义NestingNavigators.js文件,在App.js中引入即可 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';

function Feed() {
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#e3e',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Feed Screen</Text>
      <Button
        title="Go to Profile"
        onPress={() => navigation.navigate('Profile')}
      />
    </View>
  );
}

function Messages() {
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#b33',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Messages Screen</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

function Home() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Feed" component={Feed} />
      <Tab.Screen name="Messages" component={Messages} />
    </Tab.Navigator>
  );
}

function Profile() {
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#a3e',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Profile Screen</Text>
    </View>
  );
}

function Settings() {
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#e3a',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Settings Screen</Text>
    </View>
  );
}

const Stack = createStackNavigator();

function NestingNavigators() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="Profile" component={Profile} />
        <Stack.Screen name="Settings" component={Settings} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default NestingNavigators;

(注:官方文档代码省略了很多,我这边弄了个完整的,有兴趣的童鞋也可以自己实现!)

在上面的例子中,Home组件包含了一个tab导航。在整个App组件中,Home组件也被用在stack导航的Home页面上。因此,这里的tab导航是嵌套在stack导航里面的:

* Stack.Navigator
   。Home (Tab.Navigator)
      . Feed (Screen)
      . Messages (Screen)
   。Profile (Screen)
   。Settings (Screen)

嵌套导航器的工作原理非常类似于嵌套常规组件。为了实现您想要的行为,通常需要嵌套多个导航器。

7.1 嵌套导航有什么影响

当嵌套导航时,有些东西要注意:

7.1.1 每个导航器保存自己的导航历史

比如,在一个嵌套的stack导航里点击了返回按钮,它将返回到嵌套的栈内的上一个页面,即使有另一个导航器作为父视图。

7.1.2 导航操作由当前导航器处理,如果无法处理就由它的父导航处理

例如,如果你在一个嵌套的页面上调用navigation.goBack(),如果已经在第一个页面上了,那么仅仅会返回到父导航。其他操作比如navigate在效果上是一样的,例如,在嵌套的导航里操作导航,但这个操作没有处理,然后父导航将会处理这个操作。在上例中,当在Feed页面上调用navigate('Messages'),这个嵌套的tab导航会处理它,但是如果你调用navigate('Settings'),父栈导航器将会处理它。

7.1.3 嵌套导航不接收父事件

例如,你有一个栈导航嵌套在tab导航里,在栈导航上的页面不会收到父tab导航发出的通知事件,比如(tabPress)时添加navigation.addListener监听。为了能收到父导航的事件,你可以使用navigation.dangerouslyGetParent().addListener来监听父事件。

7.1.4 父导航的UI渲染在子导航的顶部

例如,当嵌套一个栈导航到折叠(抽屉)导航里,你会看到折叠页面在栈导航的头部。然而,如果你嵌套一个折叠导航到栈导航里,折叠页面会出现在header下面。这是一个很重要的点,对于你决定如何嵌套导航。

在你的App中,你可能会根据你想要的行为使用这些模式:

  • 栈导航嵌套在折叠导航的每个页面中-折叠页面出现在栈的header上面。
  • Tab导航嵌套在栈导航的初始页面里-当push页面时,新页面会覆盖tab bar。
  • 栈导航嵌套在tab导航的每个页面中-tab bar总是可见的。点击tab会再次pops到栈顶。

7.2 导航到一个嵌套导航里的页面

考虑下面的例子:

function Root() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Settings" component={Settings} />
    </Stack.Navigator>
  );
}

const Drawer = createDrawerNavigator();

function NestingNavigators() {
  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。但有时,你可能想在导航上控制想展示的页面。通过下面操作,你能在参数里指定页面的名称:

navigation.navigate('Root', { screen: 'Settings' });

现在,Settings将会代替Profile展示在导航上。

这看起来可能与以前使用嵌套页面的导航方式非常不同。与之前的差异是,所有的配置都是静态的,因此,React Navigation可以通过递归到嵌套配置中,静态地找到所有导航器及其页面的列表。但是使用动态配置,React Navigation不知道哪些页面可用,在哪里可用,直到导航器包含页面渲染后。通常,还没导航到一个页面的时候,这个页面的内容不会被渲染,所以还没有渲染的导航器的配置是不可用的。这就需要指定导航到的层次结构。这也是为什么你应该使用尽可能少的导航器嵌套以使代码更简单的原因。

7.2.1 在嵌套导航传递参数到页面

你也可以通过一个指定的参数key传递参数:

navigation.navigate('Root', {
  screen: 'Settings',
  params: { user: 'jane' },
});

在Snack上尝试编写

如果导航已经渲染,在使用栈导航时跳转到另一个页面就会push到一个新的页面。

对于深度嵌套的屏幕,可以采用类似的方法。注意navigate的第二个参数,也就是params,也可以这样做:

navigation.navigate('Root', {
  screen: 'Settings',
  params: {
    screen: 'Sound',
    params: {
      screen: 'Media',
    },
  },
});

上面的案例中,你想要导航到Media页面,他是嵌套在Sound页面里面的,Sound页面又嵌套在Settings页面里。

7.2.2 在导航器里渲染被定义的初始路由

默认情况下,当导航到一个嵌套在导航里的页面时,被指定的页面会被用于初始页面,并且初始路由属性会被忽略。这个不同于React Navigation 4。

如果你想在导航里想渲染一个指定的初始页面,通过设置initial: false,您可以禁用使用指定页面作为初始页面的行为:

navigation.navigate('Root', {
  screen: 'Settings',
  initial: false,
});

7.3 嵌套时的最佳实践

我们推荐减少嵌套导航到最小。尝试用尽可能少的嵌套来实现您想要的行为。嵌套有很多缺点:

  • 当导航到嵌套的页面时,代码变得难以理解
  • 它会导致深度嵌套的视图层次结构,会导致低端设备的内存和性能问题
  • 嵌套相同类型的导航器(例如tab在tab里,drawer在drawer里等等)会导致令人困惑的用户体验。

将嵌套导航器看作是实现您想要的UI的一种方式,而不是组织代码的一种方式。如果您希望为组织创建单独的页面组,请将它们保存在单独的对象/数组中,而不是保存在单独的导航器中。

8 Navigation生命周期

在前面的部分,我操作了一个包含Home和Details两个页面的栈导航,并且使用navigation.navigate('RouteName')在路由间导航。

在这方面的一个重要问题是:当我们离开Home页面时或返回到Home页面时发生了什么?用户如何知道离开了或返回到一个路由页面?

如果您从web角度来看react-navigation,那么您可以假设当用户从路由a导航到路由B时,A将被卸载(componentWillUnmount会被调用),当返回到A时,又会被加载。而这些React生命周期方法仍然有效并被使用在react-navigation,它们的用法和web是有区别的。这是由更复杂的移动导航需求驱动的。

8.1 示例场景

有一个包含A和B页面的栈导航。导航到A页面后,它的componentDidMount函数被调用。当push到B页面,它的componentDidMount也被调用,但是A仍然挂载在栈上,因此不调用它的componentWillUnmount。

当从B页面回到A时,B页面的componentWillUnmount被调用,但A的componentDidMount不会被调用,因为A在整个生命周期一直在栈上。

与其他导航器(结合使用)也可以观察到类似的结果。考虑一个tab导航上有两个tab,每个tab都在栈导航上:

/* Navigation生命周期 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
/*这里就省略了,减少篇幅,具体代码可以在前面代码里找*/
function Profile() {...}
function Settings() {...}
function HomeScreen() {...}
function DetailScreen() {...}

const Tab = createBottomTabNavigator();
const SettingsStack = createStackNavigator();
const HomeStack = createStackNavigator();

function NavigationLifeCycle() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="First">
          {() => (
            <SettingsStack.Navigator>
              <SettingsStack.Screen name="Settings" component={Settings} />
              <SettingsStack.Screen name="Profile" component={Profile} />
            </SettingsStack.Navigator>
          )}
        </Tab.Screen>
        <Tab.Screen name="Second">
          {() => (
            <HomeStack.Navigator>
              <HomeStack.Screen name="Home" component={HomeScreen} />
              <HomeStack.Screen name="Details" component={DetailScreen} />
            </HomeStack.Navigator>
          )}
        </Tab.Screen>
      </Tab.Navigator>
    </NavigationContainer>
  );
}

export default NavigationLifeCycle;

在Snack上尝试编写

刚开始在HomeScreen,然后导航到DetailsScreen。然后切换tab展示SettingsScreen页面,再导航到ProfileScreen。这部分操作已经完成,4个页面都已经加载了!如果你用tab切换回HomeStack,你将会看到DetailsScreen页面-HomeStack已经保存了导航状态。

8.2 React Navigation 的生命周期事件

现在我们已经了解了React生命周期方法是如何在React Navigation中工作的,让我们解答一下刚开始的问题:”用户如何知道离开(blur)了或返回(focus)到一个路由页面?“

React Navigation向订阅事件的页面组件发出事件。我们可以监听focus和blur事件,就可以知道是否进入当前页面或离开了当前页面。

例如:

function Profile({navigation}) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      //Screen was focused
      //Do sometings
      console.log('Profile page is focused!!!');
    });
    return unsubscribe;
  }, [navigation]);
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#a3e',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Profile Screen</Text>
    </View>
  );
}

在Snack上尝试编写

参阅Navigation events了解更多事件和API的用法

我们可以使用useFocusEffect来执行,而不是手动添加事件监听器。它类似于React的useEffect,但它与导航生命周期紧密相连。

例如:

import { useFocusEffect } from '@react-navigation/native';

function Profile({navigation}) {
  useFocusEffect(
    React.useCallback(() => {
      //Do something when the screen is focused
      console.log('Profile page is focused!!!');
      return () => {
        //Do something when the screen is unfocused
        // Useful for cleanup functions
        console.log('Profile page is unfocused!!!');
      };
    }, []),
  );
  return (
    <View
      // eslint-disable-next-line react-native/no-inline-styles
      style={{
        flex: 1,
        backgroundColor: '#a3e',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Profile Screen</Text>
    </View>
  );
}

在Snack上尝试编写

如果你想根据页面是否被聚焦来渲染不同的东西,你可以使用useIsFocused,它返回一个指示页面是否被聚焦的布尔值。

8.3 总结

  • 虽然React的生命周期方法仍然有效,但React导航添加了更多的事件,可以通过navigation属性来订阅。
  • 你也可以用useFocusEffect或者useIsFocused来代替。

9 打开full-screen模态

模态显示临时的与主视图交互的内容。

模态类似于一个弹框 -- 它不是你的主要导航流程的一部分 -- 它通常有一个不同的过渡,不同的方法dismiss它,并且专注于某一特定的内容或交互。

将其作为React导航基础原理的一部分进行解释的目的不仅仅是因为这是一个常见的用例,还因为需要嵌套导航器的知识来实现,它是React Navigation重要的一部分。

9.1 创建模态栈

/* Navigation生命周期 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

function HomeScreen({navigation}) {
  return (
    // eslint-disable-next-line react-native/no-inline-styles
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text style={{fontSize: 30}}>This is the home screen!</Text>
      <Button
        title="Open Modal"
        onPress={() =>
          /* 1.传递参数到详情页面 */
          navigation.navigate('MyModal')
        }
      />
    </View>
  );
}

function DetailScreen() {
  return (
    // eslint-disable-next-line react-native/no-inline-styles
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Detail Screen</Text>
    </View>
  );
}

function ModalScreen({navigation}) {
  return (
    // eslint-disable-next-line react-native/no-inline-styles
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text style={{fontSize: 30}}>This is a Modal!</Text>
      <Button title="Dismiss" onPress={() => navigation.goBack()} />
    </View>
  );
}

const MainStack = createStackNavigator();
const RootStack = createStackNavigator();

function MainStackScreen() {
  return (
    <MainStack.Navigator>
      <MainStack.Screen name="Home" component={HomeScreen} />
      <MainStack.Screen name="Details" component={DetailScreen} />
    </MainStack.Navigator>
  );
}

function NavigationModal() {
  return (
    <NavigationContainer>
      <RootStack.Navigator mode="modal">
        <RootStack.Screen
          name="Main"
          component={MainStackScreen}
          options={{headerShown: false}}
        />
        <RootStack.Screen
          name="MyModal"
          component={ModalScreen}
          options={{headerShown: false}}
        />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

export default NavigationModal;

(注:官方文档里提供的代码有点问题,我这边完善了一下)

在Snack上尝试编写

有些重要的事情要注意一下:

  • 我们用MainStackScreen组件作为RootStackScreen的一个页面!这里,我们是嵌套了一个栈导航在另一个栈导航里。这样做是完全有效的,因为我们想要使用模态实现不同的过渡方式。由于RootStackScreen渲染一个栈导航器,并有自己的header,我们也想隐藏这个页面的header。在将来它将是重要的,对于tab导航,例如,每个tab会有自己的栈。直观上,这就是你所期望的:当你从tab A切换到tab B,在继续浏览tab B时,您希望tab A保持其导航状态。请看这张图,在这个例子中可以看到导航的结构:

    image.png

  • 在栈导航上,mode属性可以是:card(默认)和 modal。在iOS上,modal的行为从底部滑动屏幕,并允许用户从顶部向下滑动来关闭它。modal属性在Android上没有影响,因为全屏模式在平台上没有任何不同的转换行为。

  • 当我们调用navigate时,我们不需要指定任何东西除了我们想要导航到的路由。不需要限定它属于哪个堆栈(任意命名的“根”或“主”堆栈)——React导航尝试在最近的导航器上查找路由,然后在那里执行操作。为了使其可视化,请再次查看上面的树形图,并想象navigate动作从主屏幕流向主堆栈。我们知道MainStack不能处理路由MyModal,所以它会将它流到RootStack,后者能处理那个路由,所以它确实做到了。

9.2 总结

  • 在栈导航上想要改变过渡方式,可以使用mode属性。当设置为modal时,所有的屏幕都是动态的——从下到上而不是从右到左。这适用于整个堆栈导航器,因此为了在其他页面上使用从右到左的转换,我们添加另一个具有默认配置的导航堆栈。

  • navigation.navigate导航遍历导航器树以查找可以处理导航操作的导航器。

10 专业术语

这是文档的新部分,缺少很多术语! 请提交拉取请求或您认为应该在此处解释的术语问题。

10.1 Header

也称为navigation header,navigation bar,navbar,可能还有许多其他内容。 这是屏幕顶部的矩形,其中包含后退按钮和屏幕标题。 整个矩形在React Navigation中通常称为标头。

10.2 Navigator

导航器包含Screen元素作为其子元素,以定义路由的配置。 NavigationContainer是管理导航树并包含导航状态的组件。 该组件必须包装所有导航器结构。 通常,我们会在应用程序的根目录下渲染此组件,通常是从App.js导出的组件。

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator> // <---- This is a Navigator
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

10.2 Screen component

屏幕组件是我们在路由配置中使用的组件。

const Stack = createStackNavigator();

const StackNavigator = (
  <Stack.Navigator>
    <Stack.Screen
      name="Home"
      component={HomeScreen} // <----
    />
    <Stack.Screen
      name="Details"
      component={DetailsScreen} // <----
    />
  </Stack.Navigator>
);

组件名称中的后缀Screen完全是可选的,但是是经常使用的约定。 我们可以称其为Michael,它的工作原理相同。

前面我们看到,我们的屏幕组件是随导航prop一起提供的。 重要的是要注意,仅当屏幕由React Navigation渲染为路由时(例如,响应于navigation.navigate),才会发生这种情况。 例如,如果我们将DetailsScreen作为HomeScreen的子级呈现,则DetailsProperty将不会随导航prop一起提供,并且当您在主屏幕上按“再次转到Details ...”按钮时,该应用将抛出一个 典型的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 reference" 部分对此进行了更详细的介绍,介绍了解决方法,并提供了有关导航prop上其他可用属性的更多信息。

10.3 Navigation Prop

该prop将传递到所有页面,可用于以下用途:

  • dispatch将向路由器发送一个动作
  • navigate,goBack等可用于以方便的方式调度操作

导航器也可以接受导航prop,如果有的话,它们应该从父导航器获得。

更多细节,请参考"Navigation prop document".

"Route prop reference" 部分对此进行了更详细的介绍,介绍了解决方法,并提供了有关路由prop上其他可用属性的更多信息。

10.4 Route prop

该prop将传递到所有页面。 包含有关当前路由的信息,即参数,key和name。

10.5 Navigation State

导航器的状态通常如下所示:

{
  key: 'StackRouterRoot',
  index: 1,
  routes: [
    { key: 'A', name: 'Home' },
    { key: 'B', name: 'Profile' },
  ]
}

对于此导航状态,有两个路由(可以是tabs或堆栈中的cards)。 索引指向活动路由,即“ B”。

10.6 Route

每个路由都是一个导航状态,其中包含一个用于识别它的键以及一个用于指定路由类型的“名称”。 它还可以包含任意参数:

{
  key: 'B',
  name: 'Profile',
  params: { id: '123' }
}

11 兼容层

注意:在遵循本指南之前,请确保您已按照入门指南在应用程序中设置React Navigation 5。

React Navigation 5具有全新的API,因此我们使用React Navigation 4的旧代码将不再适用于该版本。 如果您不熟悉新的API,则可以阅读升级指南中的差异。 我们知道这可能需要做很多工作,因此我们创建了一个兼容性层来简化此过程。

使用这个兼容性层,需要安装@react-navigation/compat:

  • npm
npm install @react-navigation/native @react-navigation/compat @react-navigation/stack
  • yarn
yarn add @react-navigation/native @react-navigation/compat @react-navigation/stack

然后在我们的代码里要做一些小小的改动:

//“-”代表删除,“+”代表添加
-import { createStackNavigator } from 'react-navigation-stack';
+import { createStackNavigator } from '@react-navigation/stack';
+import { createCompatNavigatorFactory } from '@react-navigation/compat';
-const RootStack = createStackNavigator(
+const RootStack = createCompatNavigatorFactory(createStackNavigator)(
  {
    Home: { screen: HomeScreen },
    Profile: { screen: ProfileScreen },
  },
  {
    initialRouteName: 'Profile',
  }
);

如果之前导入的是react-navigation,那么现在要修改一下,导入 @react-navigation/compat:

//“-”代表删除,“+”代表添加
-import { NavigationActions } from 'react-navigation'; 
+import { NavigationActions } from '@react-navigation/compat';

该库导出以下API:

  • Actions: -> NavigationActions -> StackActions -> DrawerActions -> SwitchActions
  • HOCs -> withNavigation -> withNavigationFocus
  • Navigators -> createSwitchNavigator
  • Compatibility helpers -> createCompatNavigatorFactory - 使用v5 API的导航器,并使用v4 API返回createXNavigator。 -> createCompatNavigationProp - 将v5导航对象与route对象一起使用,并返回v4导航对象。

11.1 它处理什么?

兼容性层处理React Navigation 4和5之间的各种API差异:

  • 使用v4的静态配置API代替基于组件的API。
  • 更改导航对象上方法的签名以匹配v4。
  • 添加对在5版中删除的screenProps的支持。
  • 导出action创建者,例如NavigationActions,StackActions,SwitchActions,其签名与v4相同。

11.2 它不处理什么?

由于React Navigation 5的动态API,v4的静态API不再具有某些功能,因此兼容性层无法处理它们:

  • 它不包装导航器的prop或options。 基本上,这意味着您要传递给导航器的options可能会因导航器中的重大更改而有所不同。 请参阅导航器的文档以获取更新选项API。
  • 不支持通过在路由配置中定义路径来支持旧式深层链接。 请参阅deep linking documentation 以获取更多详细信息,现在如何处理深层链接。
  • 导航到导航器的工作原理不同,即,我们无法导航到尚未渲染的导航器中的屏幕,并且无法将参数合并到所有子屏幕。 有关如何导航到其他导航器中的屏幕的更多详细信息,请参见嵌套导航器文档。
  • 不再支持某些采取一系列操作的方法,例如旧式重置方法。 不支持的方法在使用它们时会抛出错误,如果我们使用的是TypeScript,则会给出类型错误。
  • 它不会导出createAppContainer,因此您需要为容器(NavigationContainer)使用v5 API。 这也意味着容器支持的所有功能都需要迁移到新的API。
  • 如果您使用的是Redux集成,自定义路由器和操作等高级API,则不再支持它们,则需要删除Redux集成。

尽管我们已尽最大努力使兼容性层处理大多数差异,但可能会丢失某些内容。 因此,请确保测试已迁移的代码。

为什么我们应该使用它?

使用兼容性层可以使我们将代码逐步迁移到新版本。 不幸的是,我们确实必须更改一些代码才能使兼容性层正常工作(请参阅“它不处理的内容”),但是它仍然允许我们的大多数代码保持不变。 使用兼容层的一些优点包括:

  • 它允许我们使用新API编写新代码,同时使用旧版API与代码集成,即,您可以从用新API编写的代码导航到用旧版API定义的屏幕,反之亦然。
  • 由于它建立在具有出色TypeScript支持的v5之上,因此旧代码也可以利用改进的类型检查功能,当您以后想要将其重构为新API时,这将非常有用。
  • 您可以详细了解迁移情况,例如 仅将组件中的几种方法迁移到新的API。 您仍然可以访问navigation.original上的v5导航对象,可用于逐步迁移代码。
  • 您可以访问旧组件中的新API,例如navigation.setOptions或新hooks,例如useFocusEffect。

我们致力于帮助您尽可能轻松地进行升级。 因此,请提出有关兼容性层不支持的用例的问题,以便我们找出一个好的迁移策略。

12 故障排除

本节试图概述用户首次习惯使用React Navigation时经常遇到的问题。 这些问题可能与React Navigation本身有关,也可能无关。

解决问题之前,请确保已升级到软件包的最新可用版本。 您可以通过再次安装软件包来安装最新版本(例如npm install package-name)。

12.1 更新了最新版本,得到“Unable to resolve module”错误

有三个原因:

Metro捆绑器的过时缓存

如果模块指向本地文件(即模块名称以./开头),则可能是由于过时的缓存所致。 要解决此问题,请尝试以下解决方案。

如果使用Expo,运行:

expo start -c

如果使用的不是Expo,运行:

npx react-native start --reset-cache

如果都不起作用,请做如下操作:

rm -rf $TMPDIR/metro-bundler-cache-*

12.2 缺少对等依赖

如果模块指向一个npm包(即模块的名称不带./),则可能是由于缺少对等项依赖关系引起的。 要解决此问题,请在项目中安装依赖项:

  • npm
npm install name-of-the-module
  • yarn
yarn add name-of-the-module

有时甚至可能是由于安装损坏导致的。 如果清除缓存不起作用,请尝试删除您的node_modules文件夹,然后再次运行npm install。

12.3 metro配置中缺少扩展

有时报错是这样的:

Error: While trying to resolve module "@react-navigation/native" from file "/path/to/src/App.js", the package "/path/to/node_modules/@react-navigation/native/package.json" was successfully found. However, this package itself specifies a "main" module field that could not be resolved ("/path/to/node_modules/@react-navigation/native/src/index.tsx"

如果您具有Metro的自定义配置并且未将ts和tsx指定为有效扩展名,则可能会发生这种情况。 这些扩展名存在于默认配置中。 要检查是否存在此问题,请在项目中查找metro.config.js文件,并检查是否已指定sourceExts选项。 它至少应具有以下配置:

sourceExts: ['js', 'json', 'ts', 'tsx'];

如果缺少这些扩展,请添加它们,然后如上节所示清除Metro缓存。

12.4 报错“SyntaxError in @react-navigation/xxx/xxx.tsx”或“SyntaxError: /xxx/@react-navigation/xxx/xxx.tsx: Unexpected token”

如果您使用的是Metro-react-native-babel-preset软件包的旧版本,则可能会发生这种情况。 修复它的最简单方法是删除node_modules以及锁定文件并重新安装依赖项。

如果使用的是npm:

rm -rf node_modules
rm package-lock.json
npm install

如果使用的是yarn:

rm -rf node_modules
rm yarn.lock
yarn

您可能还需要按照页面前面的说明清除Metro bundler的缓存。

12.5 报错“Module '[...]' has no exported member 'xxx' when using TypeScript”

如果您的项目中有旧版本的TypeScript,则可能会发生这种情况。 您可以尝试升级它:

如果使用的是npm:

npm install --save-dev typescript

如果使用的是yarn:

yarn add --dev typescript

12.6 报错“null is not an object (evaluating 'RNGestureHandlerModule.default.Direction')”

如果您未链接react-native-gesture-handler库,则可能会发生此错误和一些类似的错误。

从React Native 0.60开始自动链接,所以如果您手动链接了库,请先取消链接:

react-native unlink react-native-gesture-handler

如果要在iOS上测试并使用Mac,请确保已在ios /文件夹中运行pod install:

cd ios
pod install
cd ..

现在,重新编译app并在您的设备或模拟器上进行测试。

12.7 添加视图后,屏幕上看不到任何内容

如果将容器包装在View中,请确保使用flex:1,将View拉伸以填充容器。

import * as React from 'react';
import { View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <View style={{ flex: 1 }}>
      <NavigationContainer>{/* ... */}</NavigationContainer>
    </View>
  );
}

12.8 警告:“Non-serializable values were found in the navigation state”

如果您在参数中传递了不可序列化的值,例如类实例,函数等,则会发生这种情况。 在这种情况下,React Navigation警告您,因为这可能会破坏其他功能,例如状态持久性,深层链接等。

在参数中传递函数的常见用例示例如下:

  • 传递要在标题按钮中使用的回调。 可以使用navigation.setOptions代替。 有关示例,请参见guide for header buttons
  • 将回调传递到下一个屏幕,可以调用该屏幕将一些数据传回。 通常,您可以使用导航来实现。 有关示例,请参见指南中的guide for params
  • 将复杂数据传递到另一个屏幕。 无需传递数据参数,您可以将该复杂数据存储在其他位置(例如全局存储),并传递一个ID。 然后,屏幕可以使用ID从全局存储中获取数据。

如果您不使用状态持久性或不使用接受参数形式的屏幕的深层链接,则警告不会影响您,您可以放心地忽略它。 要忽略警告,可以使用YellowBox.ignoreWarnings。

例如:

import { YellowBox } from 'react-native';

YellowBox.ignoreWarnings([
  'Non-serializable values were found in the navigation state',
]);

12.9 连接到Chrome调试器后,应用无法正常运行

当应用程序连接到Chrome调试器(或其他使用Chrome调试器的工具,例如React Native Debugger)时,您可能会遇到与计时相关的各种问题。

这可能会导致诸如按钮按下需要花费很长时间才能注册或根本无法工作,手势和动画缓慢且有错误等问题。还可能存在其他功能问题,例如promises无法解决,超时和间隔无法正常工作等。也一样

这些问题与React Navigation不相关,而是由于Chrome调试器的工作原理所致。连接到Chrome调试器后,您的整个应用程序将在Chrome上运行,并通过网络上的sockets与本机应用程序进行通信,这可能会导致延迟和与计时相关的问题。

因此,除非您尝试进行调试,否则最好在不连接Chrome调试器的情况下测试该应用。如果您使用的是iOS,则可以使用Safari调试应用程序,该应用程序可以直接在设备上调试该应用程序,并且没有这些问题,尽管它还有其他缺点。

13 局限性

作为库的潜在用户,重要的是要知道您可以使用和不能使用它。 有了这些知识,您可以选择a different library instead。 我们将在 pitch & anti-pitch讨论高级设计决策,这里我们将介绍一些用例,这些用例要么不被支持,要么很难完成,以至于不可能。 如果以下任何限制是您应用的破坏因素,则React Navigation可能不适合您。

13.1 有限的从右到左(RTL)布局支持

我们试图在React Navigation中正确处理RTL布局,但是从事React Navigation的团队规模很小,目前我们没有带宽或流程来测试所有针对RTL布局的更改。 因此,您可能会遇到RTL布局问题。

如果您喜欢React Navigation必须提供的功能,但由于此限制而被关闭,我们鼓励您参与其中并获得RTL布局支持的所有权。 请在Twitter上与我们联系:@reactnavigation

13.2 一些特定于平台的行为

React Navigation不支持具有3D触摸功能的设备上提供的窥视和弹出功能。

下一章节:RN路由-React Navigation--Tab navigation

参考文档:React Navigation - Fundamentals