# 嵌套导航

嵌套导航器意味着在另一个导航器的屏幕中渲染一个导航器,例如:

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} />
        <Stack.Screen name="Profile" component={Profile} />
        <Stack.Screen name="Settings" component={Settings} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

在上面的例子中,Home 组件包含一个选项卡导航器(tab navigator)。Home 组件也用于 App 组件内堆栈导航器中的 Home 屏幕。在这里,选项卡导航器嵌套在堆栈导航器中:

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

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

# 嵌套导航器如何影响行为

嵌套导航器时,需要记住以下几点:

# 每个导航器保持自己的导航历史

例如,当你在一个嵌套堆栈导航器的屏幕中按后退按钮时,它会回到嵌套堆栈中的上一个屏幕,即使有另一个导航器作为父类。

# 每个导航器都有自己的选项

例如,在嵌套在子导航器中的屏幕中指定 title 选项不会影响父导航器中显示的标题。

如果您想实现这种行为,请参阅带有嵌套导航器的屏幕选项的指南 (opens new window)。如果您正在堆栈导航器中呈现一个TAB导航器,并且希望在堆栈导航器头部的TAB导航器中显示活动屏幕的标题,那么这可能是有用的。

# 导航器中的每个屏幕都有自己的参数

例如,在嵌套导航器中传递给屏幕的任何 params 都在该屏幕的 route 属性中,并且不能从父或子导航器中的屏幕访问。

如果你需要从子屏幕访问父屏幕的参数,你可以使用 React Context (opens new window) 向子屏幕公开参数。

# 导航操作由当前导航器处理,如果不能处理,则向上冒泡

例如,如果你在嵌套屏幕中调用 navigation.goBack(),它只会在父导航器中返回,如果你已经在导航器的第一个屏幕上。其他操作,与 navigate 工作类似,即导航将发生在嵌套导航器,如果嵌套导航器不能处理它,那么父导航器将尝试处理它。在上面的例子中,当调用 navigate('Messages') 时,在 Feed 屏幕内,嵌套的标签导航器将处理它,但如果你调用 navigate('Settings'),父堆栈导航器将处理它。

# 导航器的特定方法可以在内部嵌套的导航器中使用

例如,如果你在一个抽屉导航器中有一个堆栈导航器,那么这个抽屉的 openDrawer, closeDrawer, toggleDrawer 等方法也可以在屏幕的堆栈导航器内部的 navigation prop 中使用。但假设你有一个堆栈导航器作为抽屉的父类,那么堆栈导航器中的屏幕将不能访问这些方法,因为它们没有嵌套在抽屉中。

类似地,如果你在堆栈导航器中有一个选项卡导航器,选项卡导航器中的屏幕将在它们的 navigation 属性中获得堆栈的 pushreplace 方法。

如果你需要从父导航器中分派操作到嵌套的子导航器,你可以使用 navigation.dispatch:

navigation.dispatch(DrawerActions.toggleDrawer());

# 嵌套的导航器不接收父类的事件

例如,如果一个堆栈导航器嵌套在一个选项卡导航器中,当使用 navigation.addListener 时,堆栈导航器中的屏幕将不会接收父标签导航器(如tabPress)发出的事件。

为了从父导航器接收事件,你可以使用 navigation.dangerouslyGetParent() 显式监听父导航器的事件:

const unsubscribe = navigation
  .dangerouslyGetParent()
  .addListener('tabPress', (e) => {
    // Do something
  });

# 父导航器的UI呈现在子导航器之上

例如,当您在抽屉导航器中嵌套堆栈导航器时,您将看到抽屉导航器出现在堆栈导航器页眉的上方。但是,如果将抽屉导航器嵌套在堆栈中,则抽屉导航器将出现在堆栈页眉的下方。这是在决定如何嵌套导航器时需要考虑的重要一点。

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

  • 选项卡导航器嵌套在堆栈导航器的初始屏幕-新屏幕覆盖 tab bar 时,你 push 它们。
  • 抽屉导航器嵌套在堆栈导航器的初始屏幕中,初始屏幕的堆栈头被隐藏 - 抽屉只能从堆栈的第一个屏幕打开。
  • 堆栈导航器嵌套在抽屉导航器的每个屏幕中 — 抽屉显示在堆栈导航器页眉上方。
  • 堆栈导航器嵌套在标签导航器的每个屏幕-标签栏总是可见的。通常,再次按下选项卡也会将堆栈弹出到顶部。

# 在嵌套导航器中导航到屏幕

考虑下面例子

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

function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator>
        <Drawer.Screen name="Home" component={Home} />
        <Drawer.Screen name="Root" component={Root} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

在这里,你可能想要从你的 Home 组件导航到 Root 组件:

navigation.navigate('Root');

它工作了,并且显示了 Root 组件内部的初始屏幕,即 Profile。但有时您可能想要控制应该在导航时显示的屏幕。为了实现它,你可以在 params 中传递屏幕名称:

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

现在,Settings 屏幕将在导航时呈现,而不是 Profile

这看起来可能与以前用于嵌套屏幕的导航方式非常不同。不同的是,在以前的版本中,所有的配置都是静态的,所以React Navigation可以通过递归到嵌套的配置中,静态地找到所有导航器的列表和它们的屏幕。但是对于动态配置,React Navigation 并不知道哪些屏幕可用,直到包含屏幕的导航器渲染。通常,一个屏幕在导航到它之前不会渲染它的内容,所以还没有渲染的导航器配置是不可用的。这就需要指定要导航到的层次结构。这也是为什么您应该尽可能少地嵌套导航器以使代码更简单的原因。

# 在嵌套导航器中向屏幕传递参数

你也可以通过指定 params key 来传递参数:

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

如果导航器已经x渲染,在堆栈导航器的情况下,导航到另一个屏幕将推入一个新屏幕。

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

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

在上述情况下,您正在导航到 Media 屏幕,它位于嵌套在 Sound 屏幕中的导航器中,而 Sound 屏幕又位于嵌套在 Settings 屏幕中的导航器中。

# 渲染导航器中定义的初始路由

默认情况下,当在嵌套导航器中导航屏幕时,指定的屏幕被用作初始屏幕,导航器上的初始路由属性将被忽略。这种行为不同于 React Navigation 4。

如果你需要渲染导航器中指定的初始路由,你可以通过设置 initial: false : 来禁用使用指定屏幕作为初始屏幕的行为。

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

# 嵌套多个导航器

有时,嵌套多个导航器(如堆栈或抽屉)是有用的,例如,将一些屏幕放在模态堆栈中,而将一些屏幕放在常规堆栈中 (opens new window)

当嵌套多个堆栈或抽屉导航器时,将显示来自子导航器和父导航器的头。然而,通常更可取的做法是在子导航器中显示头文件,而在堆栈导航器中隐藏头文件。

为了实现这一点,您可以使用 headerShown: false 选项在包含导航器的屏幕中隐藏标题。

举个栗子:

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

function App() {
  return (
    <NavigationContainer>
      <RootStack.Navigator mode="modal">
        <RootStack.Screen
          name="Home"
          component={Home}
          options={{ headerShown: false }}
        />
        <RootStack.Screen name="EditPost" component={EditPost} />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

完整的例子可以在 模态指南 (opens new window) 中找到。然而,这个原则并不只适用于模态堆栈,也适用于任何嵌套的导航器。

在这些例子中,我们使用了一个直接嵌套在另一个堆栈导航器中的堆栈导航器,但是同样的原理也适用于中间有其他导航器的情况,例如:在另一个堆栈导航器中的抽屉导航器中的堆栈导航器,在抽屉导航器中的堆栈导航器等等。

当嵌套多个堆栈导航器时,除非绝对必要,我们建议最多嵌套2个堆栈导航器。

# 嵌套时的最佳实践

我们建议尽量减少嵌套导航器。尝试用尽可能少的嵌套来实现您想要的行为。嵌套有很多缺点:

  • 它会导致深度嵌套的视图层次结构,这可能导致低端设备的内存和性能问题
  • 嵌套相同类型的导航器(例如标签内的标签,抽屉内的抽屉等)可能会导致令人困惑的用户体验
  • 由于嵌套过多,代码在导航到嵌套屏幕、配置深度链接等时变得难以遵循。

将嵌套导航器看作是实现您想要的UI的一种方式,而不是组织代码的一种方式。如果你想为组织创建单独的屏幕组,而不是使用单独的导航器,可以考虑这样做:

// Define multiple groups of screens in objects like this
const commonScreens = {
  Help: HelpScreen,
};

const authScreens = {
  SignIn: SignInScreen,
  SignUp: SignUpScreen,
};

const userScreens = {
  Home: HomeScreen,
  Profile: ProfileScreen,
};

// Then use them in your components by looping over the object and creating screen configs
// You could extract this logic to a utility function and reuse it to simplify your code
<Stack.Navigator>
  {Object.entries({
    // Use the screens normally
    ...commonScreens,
    // Use some screens conditionally based on some condition
    ...(isLoggedIn ? userScreens : authScreens),
  }).map(([name, component]) => (
    <Stack.Screen name={name} component={component} />
  ))}
</Stack.Navigator>;