Expo Router 中的导航布局

编辑页面

了解如何通过使用目录和布局文件来构建页面之间的不同关系。


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

Expo Router 布局文件介绍
Expo Router 布局文件介绍

什么是布局文件、如何在屏幕之间导航,以及如何使用重定向阻止访问。

src/app 目录中的每个目录(包括 src/app 本身)都可以通过该目录内的 _layout.tsx 文件定义一个布局。这个文件定义了该目录下所有页面的排列方式。你可以在这里定义栈导航器、标签导航器、抽屉导航器,或任何你希望用于该目录页面的其他布局。布局文件会导出一个默认组件,它会在你导航到该目录中的任意页面之前先被渲染。

让我们来看几个常见的布局场景。

根布局

几乎每个应用都会在 src/app 目录中直接放置一个 _layout.tsx 文件。这就是根布局,代表你导航的入口点。除了描述应用的顶层导航器之外,这个文件也是你放置之前可能写在 App.jsx 文件中的初始化代码的地方,例如加载字体、与启动画面交互,或添加上下文提供器。

下面是一个根布局示例:

src/app/_layout.tsx
import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import { useEffect } from 'react'; SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const [loaded] = useFonts({ SpaceMono: require('@/assets/fonts/SpaceMono-Regular.ttf'), }); useEffect(() => { if (loaded) { SplashScreen.hide(); } }, [loaded]); if (!loaded) { return null; } return <Stack />; }

上面的示例会先显示启动画面,然后在字体加载完成后渲染一个栈导航器,这将使你的应用继续进入初始路由。

你可以像上面那样在根布局中实现栈导航器,也可以在目录中的任何其他布局文件中实现。假设你有如下所示、在某个目录内包含栈的文件结构:

src
app
  products
   _layout.tsx
   index.tsx
   [productId].tsx
   accessories
    index.tsx

如果你希望 src/app/products 目录中的所有内容都以栈关系排列,那么在 _layout.tsx 文件中返回一个 Stack 组件:

src/app/products/_layout.tsx
import { Stack } from 'expo-router'; export default function StackLayout() { return <Stack />; }

当你导航到 /products 时,它会先进入默认路由,也就是 products/index.tsx。如果你导航到 /products/123,那么该页面会被压入栈中。默认情况下,栈会在标题栏中渲染一个返回按钮,用于将当前页面从栈中弹出,从而返回到上一页。即使某个页面不可见,只要它仍然被压入栈中,它就仍然在被渲染。

Stack 组件实现了 React Navigation 的原生栈,并且可以使用相同的屏幕选项。不过,你不必在导航器中显式定义页面。目录中的文件会自动被视为栈中的可用路由。但是,如果你想定义屏幕选项,可以在 Stack 组件内部添加一个 Stack.Screen 组件。name 属性应与路由名称匹配,但你不需要提供 component 属性;Expo Router 会自动映射:

src/app/products/_layout.tsx
import { Stack } from 'expo-router'; export default function StackLayout() { return ( <Stack> <Stack.Screen name="[productId]" options={{ headerShown: false }} /> </Stack> ); }

虽然可以嵌套导航器,但只有在真正需要时才应这样做。在上面的示例中,如果你想将 products/accessories/index.tsx 压入栈中,那么就没有必要在 accessories 目录下额外创建一个带有 Stack 导航器的 _layout.tsx。那样会在第一个栈中再定义另一个栈。添加仅影响 URL 的目录是可以的,否则就使用与父目录相同的导航器。

标签页

Expo Router 提供了多种实现标签导航的方式,具体取决于你的需求。

JavaScript 标签页

你可以在布局文件中使用 Tabs 组件实现基于 JavaScript 的标签导航器。该目录下直接包含的所有路由都会被视为标签页。请看下面的文件结构:

src
app
  (tabs)
   _layout.tsx
   index.tsx
   feed.tsx
   profile.tsx

_layout.tsx 文件中,返回一个 Tabs 组件:

src/app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: '首页', tabBarIcon: ({ color }) => <MaterialIcons size={28} name="house.fill" color={color} />, }} /> <Tabs.Screen name="feed" options={{ title: '动态' }} /> <Tabs.Screen name="profile" options={{ title: '个人资料' }} /> </Tabs> ); }

这会使 index.tsxfeed.tsxprofile.tsx 文件一起出现在同一个底部标签导航器中。这个 Tabs 组件使用了 React Navigation 的原生底部标签,并支持相同的选项。

对于 Tabs,你通常会希望在导航器中定义这些标签,因为这会影响标签的显示顺序、标题以及标签内的图标。index 路由将是默认选中的标签页。

原生标签页

在 Android 和 iOS 上,你可以使用 原生标签页 来渲染平台内置的标签栏。原生标签页提供了符合预期的平台行为,例如点击时滚动到顶部、原生动画,以及原生的外观和体验。

与 JavaScript 标签页一样,原生标签页也可以在路由组目录中的布局文件里使用:

src
app
  (tabs)
   _layout.tsx
   index.tsx
   feed.tsx
   profile.tsx
src/app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>首页</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/home.png')} /> </NativeTabs.Trigger> <NativeTabs.Trigger name="feed"> <NativeTabs.Trigger.Label>动态</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/feed.png')} /> </NativeTabs.Trigger> <NativeTabs.Trigger name="profile"> <NativeTabs.Trigger.Label>个人资料</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/profile.png')} /> </NativeTabs.Trigger> </NativeTabs> ); }

平台特定标签页

由于原生标签页仅适用于 Android 和 iOS,一个常见模式是使用 平台特定的文件扩展名 为原生端和 web 提供不同的标签实现。根布局渲染一个标签组件,而 Expo 的模块解析会根据平台自动选择正确的文件。

src
app
  _layout.tsx
  index.tsx
  explore.tsx
components
  app-tabs.native.tsx原生标签页(Android 和 iOS)
  app-tabs.tsx自定义标签页(web)

根布局会导入并渲染 AppTabs 组件。app-tabs.native.tsx 用于 Android 和 iOS,app-tabs.tsx 用于 web:

src/app/_layout.tsx
import AppTabs from '@/components/app-tabs'; export default function RootLayout() { return <AppTabs />; }

在 Android 和 iOS 上,app-tabs.native.tsx 使用 原生标签页

src/components/app-tabs.native.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function AppTabs() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>首页</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/home.png')} /> </NativeTabs.Trigger> <NativeTabs.Trigger name="explore"> <NativeTabs.Trigger.Label>探索</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/explore.png')} /> </NativeTabs.Trigger> </NativeTabs> ); }

在 web 上,app-tabs.tsx 使用来自 expo-router/ui自定义标签页,它们是不带样式且灵活的组件:

src/components/app-tabs.tsx
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui'; export default function AppTabs() { return ( <Tabs> <TabSlot /> <TabList> <TabTrigger name="index" href="/"> 首页 </TabTrigger> <TabTrigger name="explore" href="/explore"> 探索 </TabTrigger> </TabList> </Tabs> ); }

插槽

在某些情况下,你可能希望使用不带导航器的布局。这对于在当前路由外包裹一个头部或底部,或者在某个目录内的任意路由上方显示一个模态框都很有帮助。在这种情况下,你可以使用 Slot 组件,它充当当前子路由的占位符。

请看下面的文件结构:

src
app
  social
   _layout.tsx
   index.tsx
   feed.tsx
   profile.tsx

例如,你可能希望用头部和底部将 social 目录中的任意路由包裹起来,但你又希望页面之间的导航只是简单地替换当前页面,而不是将新页面压入栈中,然后再通过“返回”导航操作将其弹出。在 _layout.tsx 文件中,返回一个由头部和底部包裹的 Slot 组件:

src/app/social/_layout.tsx
import { Slot } from 'expo-router'; export default function Layout() { return ( <> <Header /> <Slot /> <Footer /> </> ); }

其他布局

这些只是一些常见布局的示例,帮助你了解其工作方式。你还可以通过布局实现更多功能: