常见导航模式

将 Expo Router 基础知识应用于你可以在应用中使用的实际导航模式。


现在你已经了解了 Expo Router 中文件和目录的命名和排列方式,让我们运用这些知识,看看你可能在应用中使用的一些实际导航模式。

¥Now that you know the basics of how files and directories are named and arranged in Expo Router, let's apply that knowledge, looking at some real-life navigation patterns you might use in your app.

选项卡内的堆栈:嵌套导航器

¥Stacks inside tabs: nested navigators

如果你的应用通常以一组选项卡为起点,但一个或多个选项卡可能关联多个屏幕,则通常可以在选项卡内嵌套堆栈导航器。这种模式通常会生成直观的 URL,并且能够很好地扩展到桌面 Web 应用,因为桌面 Web 应用的主选项卡通常始终可见。

¥If the typical starting point for your app is a set of tabs, but one or more tabs may have more than one screen associated with it, nesting a stack navigator inside of a tab is often the way to go. This pattern often results in intuitive URLs and scales well to desktop web apps, where the primary tabs are often always visible.

考虑以下导航树:

¥Consider the following navigation tree:

app
(tabs)
  _layout.tsx
  index.tsxsingle page tab
  feed
   _layout.tsxtab with a stack inside
   index.tsx
   [postId].tsx
  settings.tsxsingle page tab

在 app/(tabs)/_layout.tsx 文件中,返回一个 Tabs 组件:

¥In the app/(tabs)/_layout.tsx file, return a Tabs component:

app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <Tabs.Screen name="settings" options={{ title: 'Settings' }} />
    </Tabs>
  );
}

在 app/(tabs)/feed/_layout.tsx 文件中,返回一个 Stack 组件:

¥In the app/(tabs)/feed/_layout.tsx file, return a Stack component:

app/(tabs)/feed/_layout.tsx
import { Stack } from 'expo-router';

export const unstable_settings = {
  initialRouteName: 'index',
};

export default function FeedLayout() {
  return <Stack />;
}

现在,在 app/(tabs)/feed 目录中,你可以拥有指向不同帖子的 Link 组件(例如 /feed/123)。这些链接会将 feed/[postId] 路由推送到堆栈,使标签导航器可见。

¥Now, within the app/(tabs)/feed directory, you can have Link components that point to different posts (for example, /feed/123). Those links will push the feed/[postId] route onto the stack, leaving the tab navigator visible.

你还可以使用相同的 URL 从任何其他选项卡导航到 feed 选项卡中的帖子。将 withAnchorinitialRouteName 结合使用,可确保 feed/index 路由始终是堆栈中的第一个屏幕:

¥You can also navigate from any other tab to a post in the feed tab with the same URL. Use withAnchor in conjunction with initialRouteName to ensure that the feed/index route is always the first screen in the stack:

app/(tabs)/feed/index.tsx
<Link href="/feed/123" withAnchor>
  Go to post
</Link>

你还可以将选项卡嵌套在外部堆栈导航器中。这通常更适合在标签页上显示模态框。

¥You can also nest tabs inside of an outer stack navigator. That is often more useful for displaying modals over the tabs.

嵌套导航器

了解更多关于如何在 Expo Router 应用中使用嵌套导航器的信息。

一屏两屏标签页:共享路由

¥One screen, two tabs: sharing routes

路由组可用于在两个不同的选项卡之间共享单个屏幕。假设导航树包含一个“Feed”选项卡和一个“Search”选项卡,它们共享用于查看用户个人资料的页面:

¥Route groups can be used to share a single screen between two different tabs. Consider a navigation tree that has a Feed tab and a Search tab, and they both share pages for viewing a user profile:

app
(tabs)
  _layout.tsx
  (feed)
   index.tsxdefault route
  (search)
   search.tsx
  (feed,search)
   _layout.tsxlayout shared between the two tabs
   users
    [username].tsxshared user profile page

每个标签页都放在一个组中,因此你可以定义第三个目录,在两个组之间共享路由 (app/(tabs)/(feed,search)/)。即使有额外的层,app/(tabs)/(feed)/index.tsx 仍然是最近的索引,因此它将是默认路由。

¥Each of the tabs is put in a group so you can define a third directory that shares routes between two groups (app/(tabs)/(feed,search)/). Even with the extra layer, app/(tabs)/(feed)/index.tsx is still the nearest index, so it will be the default route.

app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="(feed)" options={{ title: 'Feed' }} />
      <Tabs.Screen name="(search)" options={{ title: 'Search' }} />
    </Tabs>
  );
}

(feed)(search) 路由组都包含堆栈,因此它们也可以共享单个布局:

¥Both the (feed) and (search) route groups contain stacks, so they can also share a single layout:

app/(tabs)/(feed,search)/_layout.tsx
import { Stack } from 'expo-router';

export default function SharedLayout() {
  return <Stack />;
}

共享组也可以仅包含共享页面,每个不同的组都有自己的布局文件。

¥It's also possible for shared groups to only contain the shared pages, with each distinct group having its own layout file.

现在,两个标签页都可以导航到 /users/evanbacon 并显示相同的用户资料页面。

¥Now, both tabs can navigate to /users/evanbacon and see the same user profile page.

当你已将焦点放在某个选项卡上并导航到某个用户时,你将停留在当前选项卡的组中。但是,当从应用外部直接深度链接到用户个人资料页面时,Expo Router 必须从两个组中选择一个,因此它将按字母顺序选择第一个组。因此,深度链接到 /users/evanbacon 将在“Feed”选项卡中显示用户配置文件。

¥When you're already focused on a tab and navigating to a user, you will stay in that current tab's group. But when deep-linking directly to a user profile page from outside the app, Expo Router has to pick one of the two groups, so it will pick the first group alphabetically. Therefore, deep-linking to /users/evanbacon will show the user profile in the Feed tab.

共享路由

了解更多关于如何在 Expo Router 中让不同的路由共享同一个 URL 的信息。

仅经过身份验证的用户保护路由

¥Authenticated users only protecting routes

如果你有一组仅供经过身份验证的用户访问的路由,你可以将这些路由嵌入到一个组中,该组的布局会在用户未经身份验证时将用户重定向到登录页面。

¥If you have a set of routes that should only be accessible to authenticated users, you can embed those routes in a group whose layout redirects users to a login page if they are not authenticated.

其外观如下:

¥Here's what that looks like:

app
_layout.tsx
login.tsxroutes users back to /(logged-in) after authentication
(logged-in)
  _layout.tsxincludes redirect for unauthenticated users
  (tabs)
   _layout.tsx
   index.tsxdefault route for app
   feed.tsx
  modal.tsx

默认情况下,应用将转到最近的索引,即 app/(logged-in)/(tabs)/index.tsx。但是,路由组的布局文件将在路由组之前渲染。因此,如果用户未通过身份验证,此时你可以将用户重定向到登录页面:

¥By default, the app will go to the nearest index, app/(logged-in)/(tabs)/index.tsx. However, a route group's layout file will be rendered before the enclosed route is. So, if the user is not authenticated, at this point, you can redirect the user to the login page:

app/(logged-in)/_layout.tsx
import { Redirect, Stack } from 'expo-router';

export default function AuthLayout() {
  const isAuthenticated = /* check for valid auth token/session */

  if (!isAuthenticated) {
    return <Redirect href="/login" />;
  }

  return <Stack />;
}

此外,如果用户尝试导航到 (logged-in) 组内的深层链接,他们也将被重定向到登录页面。在导航到路由的过程中,每个布局都会先于路由本身进行渲染。

¥Further, if a user tries to navigate to a deep link that's inside the (logged-in) group, they will also be redirected to the login page. Every layout on the way to the route you're navigating to is first rendered before the route itself.

用于检查身份验证状态的数据源可以是 React 上下文、状态库或第三方身份验证框架。对于像 context 这样的响应式数据源,这不仅会在未经身份验证的用户首次进入应用时进行重定向,还会在用户会话失效时进行重定向,因为此布局组件会在此时重新渲染。

¥The data source for checking authentication status could be React context, a state library, or a third-party auth framework. In the case of a reactive data source like context, not only will this redirect an unauthenticated user when first entering the app, but it will also redirect them if their session becomes invalid while using the app, as this layout component will re-render at that time.

在 app/login.tsx 文件中,在用户身份验证成功后尝试将用户重定向到 /(logged-in) 路由:

¥In the app/login.tsx file, attempt to reroute the user to the /(logged-in) route after successful authentication:

app/login.tsx
import { Button } from 'react-native';
import { useRouter } from 'expo-router';

export default function Login() {
  return (
    <View>
      {/* login form */}
      <Button
        title="Login"
        onPress={() => {
          /* authenticate user */
          router.replace('/(logged-in)');
        }}
      />
    </View>
  );
}

这将导致 app/(logged-in)/_layout.tsx 再次重新渲染。此时,身份验证检查将通过,应用将继续执行默认路由。

¥This will cause app/(logged-in)/_layout.tsx to re-render again. This time, the authentication check will pass, and the app will proceed to the default route.

Expo Router 身份验证指南

关注使用受保护路由实现身份验证的深入示例。

有时最佳路由根本不是路由

¥Sometimes the best route isn't a route at all

将导航状态划分为不同的路由是为了更好地服务你和你的应用。有时,最适合该任务的模式根本不涉及导航到其他路由。由于布局文件只是 React 组件,因此你可以使用它们来显示导航器周围、旁边或替代导航器的各种 UI。

¥Separating your navigation states into distinct routes is meant to serve you and your app. Sometimes the best pattern for the job will not involve navigating to another route at all. Since layout files are just React components, you can use them to display all sorts of UI around, besides, or instead of a navigator.

回想一下身份验证,如果用户无法在未登录的情况下访问某些页面,则受保护的路由设置非常有效。但是,当未经身份验证的用户以只读模式浏览应用时该怎么办?在这种情况下,你可能希望在应用上方显示登录模式,而不是将用户重定向到登录页面:

¥Thinking back to authentication, the protected route setup works great if the user should simply not be able to visit certain pages without logging in. But what about when unauthenticated users can browse an app in read-only mode? In that case, you might want to show a login modal over the app, rather than redirecting the user to a login page:

app/(logged-in)/_layout.tsx
import { SafeAreaView, Modal } from 'react-native';
import { Stack } from 'expo-router';

export default function Layout() {
  const isAuthenticated = /* check for valid auth token / session */

  return (
    <SafeAreaView>
      <Stack />
      <Modal visible={!isAuthenticated}>{/* login UX */}</Modal>
    </SafeAreaView>
  );
}
Expo Router 中的模态框

了解在 Expo Router 中显示模态框的多种模式,包括在布局文件中使用模态框。