Expo Router 中常见的导航模式

编辑页面

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


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

既然你已经了解了 Expo Router 中文件和目录的命名与排列基础,现在让我们把这些知识应用到一些你可能会在应用中使用的真实导航模式上。

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

如果你的应用通常从一组选项卡开始,但其中一个或多个选项卡可能关联不止一个屏幕,那么在选项卡内部嵌套一个堆栈导航器通常是一个不错的选择。这种模式往往会生成直观的 URL,并且在桌面 Web 应用中也很适用,因为主选项卡通常始终可见。

请考虑以下导航树:

src
app
  (tabs)
   _layout.tsx
   index.tsx单页选项卡
   feed
    _layout.tsx内部包含堆栈的选项卡
    index.tsx
    [postId].tsx
   settings.tsx单页选项卡

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

src/app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'; export default function TabLayout() { return ( <Tabs screenOptions={{ headerShown: false }}> <Tabs.Screen name="index" options={{ title: '首页' }} /> <Tabs.Screen name="feed" options={{ title: '动态' }} /> <Tabs.Screen name="settings" options={{ title: '设置' }} /> </Tabs> ); }

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

src/app/(tabs)/feed/_layout.tsx
import { Stack } from 'expo-router'; export const unstable_settings = { initialRouteName: 'index', }; export default function FeedLayout() { return <Stack />; }

现在,在 src/app/(tabs)/feed 目录中,你可以使用指向不同帖子(例如 /feed/123)的 Link 组件。这些链接会将 feed/[postId] 路由压入堆栈,同时保留底部标签导航器可见。

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

src/app/(tabs)/feed/index.tsx
<Link href="/feed/123" withAnchor> 前往帖子 </Link>

你也可以在外层 stack 导航器中嵌套 tabs。这样通常更适合在 tabs 之上显示模态框。

嵌套导航器

了解如何在你的 Expo Router 应用中使用嵌套导航器。

每个平台不同的标签页:平台特定标签页

在构建跨平台应用时,你可能希望在 Android 和 iOS 上使用 native tabs 以获得平台原生的外观和体验,而在 web 上使用 custom tabs 来完全控制样式。你可以使用 平台特定文件扩展名 来实现这一点。

src
app
  _layout.tsx导入 AppTabs
  index.tsx
  feed.tsx
  profile.tsx
components
  app-tabs.native.tsx适用于 Android 和 iOS 的 AppTabs(native tabs)
  app-tabs.tsx适用于 web 的 AppTabs(custom tabs)

根布局会渲染一个 AppTabs 组件。Expo 的模块解析会在 Android 和 iOS 上自动选择 app-tabs.native.tsx,并在 web 上选择 app-tabs.tsx,从而让每个平台都能使用符合其习惯的标签页实现。

有关此模式的完整代码示例,请参见布局指南中的 平台特定标签页

一个屏幕,两个标签页:共享路由

路由组可用于在两个不同的标签页之间共享同一个屏幕。考虑一个导航树,它有一个 Feed 标签页和一个 Search 标签页,并且它们都共享用于查看用户资料的页面:

src
app
  (tabs)
   _layout.tsx
   (feed)
    index.tsx默认路由
   (search)
    search.tsx
   (feed,search)
    _layout.tsx两个标签页共享的布局
    users
     [username].tsx共享的用户资料页面

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

src/app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'; export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="(feed)" options={{ title: '动态' }} /> <Tabs.Screen name="(search)" options={{ title: '搜索' }} /> </Tabs> ); }

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

src/app/(tabs)/(feed,search)/_layout.tsx
import { Stack } from 'expo-router'; export default function SharedLayout() { return <Stack />; }

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

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

当你已经聚焦在某个标签页上并导航到某个用户时,你会停留在当前标签页的组中。但当你从应用外部直接深度链接到用户资料页面时,Expo Router 必须在两个组中选择一个,因此它会按字母顺序选择第一个组。因此,深度链接到 /users/evanbacon 会在 Feed 标签页中显示该用户资料。

共享路由

了解 Expo Router 中不同的路由如何共享相同的 URL。

仅限已认证用户:受保护路由

对于需要身份验证的移动应用,你很可能会有一组只能被已认证用户访问的路由。

例如,考虑下面这个导航树,其中包含一个底部标签布局、一个登录页面、一个创建账号页面,以及一个仅对已认证用户可见的模态框:

src
app
  _layout.tsx根布局
  (tabs)
   _layout.tsx
   index.tsx受保护
   settings.tsx受保护
  sign-in.tsx
  create-account.tsx
  modal.tsx受保护

当你的应用首次启动时,路由器会尝试打开根 index,也就是 src/app/(tabs)/index.tsx。如果你将这个屏幕包裹在带有 guard={false}Stack.Protected 中,该屏幕就会变得不可访问,随后会打开下一个可用屏幕。在这个例子中,由于 sign-in 屏幕是下一个可用路由,因此它会被打开。

src/app/_layout.tsx
import { Stack } from 'expo-router'; import { useAuthState } from '@/utils/authState'; export default function RootLayout() { const { isLoggedIn } = useAuthState(); return ( <Stack> <Stack.Protected guard={isLoggedIn}> <Stack.Screen name="(tabs)" /> <Stack.Screen name="modal" /> </Stack.Protected> <Stack.Protected guard={!isLoggedIn}> <Stack.Screen name="sign-in" /> <Stack.Screen name="create-account" /> </Stack.Protected> </Stack> ); }

这样,你就可以从 store 中获取认证状态并显示相应的屏幕。如果认证状态发生变化,布局会重新渲染,因此如果 isLoggedInfalse 变为 true,应用会自动导航到 (tabs) 组的根页面。

受保护路由的另一个好处是,即使你直接深度链接到某个页面,它们也会被检查。例如,如果未认证用户直接深度链接到上面的模态框屏幕,他们会被重定向到登录页面。

受保护路由也可以用于有条件地显示底部标签页。在这个例子中,vip 标签页只会向已认证且是 VIP 会员的用户显示:

src/app/(tabs)/_layout.tsx
import { Stack } from 'expo-router'; import { useAuthState } from '@/utils/authState'; export default function TabsLayout() { const { isVip } = useAuthState(); return ( <Tabs> <Tabs.Screen name="index" /> <Tabs.Protected guard={isVip}> <Tabs.Screen name="vip" /> </Tabs.Protected> <Tabs.Screen name="settings" /> </Tabs> ); }
Expo Router 身份验证

按照一份深入指南,使用受保护路由来实现身份验证。

有时,最好的路由根本不是路由

将你的导航状态拆分为不同的路由,是为了服务于你和你的应用。有时,最适合该任务的模式并不涉及跳转到另一个路由。由于布局文件本身就是 React 组件,因此你可以用它们来展示导航器周围、旁边或替代导航器的各种 UI。

回到身份验证这个话题,如果用户没有登录就不能访问某些页面,那么受保护路由的配置就非常适合。但如果未认证用户可以只读浏览应用呢?在这种情况下,你可能希望在应用上方显示一个登录模态框,而不是将用户重定向到登录页面:

src/app/(logged-in)/_layout.tsx
import { Modal } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Stack } from 'expo-router'; export default function Layout() { const isAuthenticated = /* 检查有效的 auth token / session */ return ( <SafeAreaView> <Stack /> <Modal visible={!isAuthenticated}>{/* 登录 UX */}</Modal> </SafeAreaView> ); }
Expo Router 中的模态框

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