Expo Router 路由结构完全指南:从文件约定到 Stacks/Tabs 实战(基于 building-native-ui 技能文档)
2026/9/23 9:10:11 网站建设 项目流程

Expo Router 路由结构完全指南:从文件约定到 Stacks/Tabs 实战(基于 building-native-ui 技能文档)

【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,400+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills

导读

本篇以 AAS(agentic-awesome-skills)仓库中 Expo 官方building-native-ui技能的 route-structure.md 为核心骨架,系统讲解 Expo Router 的路由文件约定、动态路由、分组路由、Stacks 与 Tabs 嵌套、Array Routes 多栈共享、布局文件与 404 处理等关键机制。读完本文,你将掌握一套可直接落地的 Expo Router 工程目录组织规范,并能结合本仓库内 SKILL.md、tabs.md、search.md 等配套参考,搭建出目录清晰、URL 简洁、支持多 Tab 独立导航历史的原生应用。

一、文件约定:app目录的黄金法则

Expo Router 以文件系统作为路由声明,所有路由都必须放置在app目录下。以下约定是构建任何 Expo Router 应用的地基,来自 route-structure.md:

  • 路由归属:路由文件一律放在app目录。
  • 动态路由:使用[]表示动态段,例如[id].tsx匹配任意单段路径。
  • 命名禁区:路由文件绝不能命名为(foo).tsx——因为(foo)是分组语法,应改用(foo)/index.tsx
  • URL 分组:使用(group)形式的目录来简化对外公开的 URL 结构(分组名不会出现在 URL 中)。
  • 禁止混放组件绝不把组件、类型或工具函数放进app目录,它们应放在components/utils/等独立目录。这是本技能反复强调的 anti-pattern。
  • 纯路由目录app目录只应包含路由文件和_layout文件,且每个文件必须默认导出一个组件
  • 根路由兜底:确保应用始终存在匹配/的路由,否则应用会呈现空白页(该路由可以位于某个分组内)。
  • Stack 必用布局:定义导航栈时一律通过_layout.tsx文件完成。

此外,SKILL.md 的代码风格一节对文件组织提出了补充要求:

  • 文件名一律使用 kebab-case,例如comment-card.tsx
  • 重构导航时必须删除旧的路由文件,避免残留路由干扰匹配;
  • 文件名中禁止使用特殊字符;
  • tsconfig.json中配置路径别名(如@/components/...),重构时优先使用别名而非相对导入。

二、动态路由:方括号声明动态段

动态路由用方括号声明可变路径段,参数名与文件名一一对应:

app/ users/ [id].tsx # 匹配 /users/123、/users/abc [id]/ posts.tsx # 匹配 /users/123/posts

[id].tsx可以匹配任意单段值(如123abc),[id]/posts.tsx则是在动态段之下的子路由,两者可以并存并各司其职。

2.1 Catch-All 路由

当一段路径需要匹配任意层级时,使用[...slug]语法:

app/ docs/ [...slug].tsx # 匹配 /docs/a、/docs/a/b、/docs/a/b/c

Catch-All 路由特别适合文档站、博客这类深度不确定的内容结构——无论/docs下嵌套多少级目录,都能被同一份代码接管并渲染。

三、Query 参数与 Pathname 读取

3.1 useLocalSearchParams:读取查询参数

在页面组件内通过useLocalSearchParams读取 URL 查询参数与动态段参数:

import { useLocalSearchParams } from "expo-router"; function Page() { const { id } = useLocalSearchParams<{ id: string }>(); }

动态路由参数的命名与文件名严格对应

  • [id].tsxuseLocalSearchParams<{ id: string }>()
  • [slug].tsxuseLocalSearchParams<{ slug: string }>()

这意味着动态段和?key=value查询串在 Expo Router 中会被统一合并进 LocalSearchParams,读取方式完全一致。

3.2 usePathname:读取当前路径

需要获取当前完整路径时使用usePathname

import { usePathname } from "expo-router"; function Component() { const pathname = usePathname(); // e.g. "/users/123" }

这在实现“当前页高亮”“面包屑导航”“埋点上报”等场景时非常有用。

四、分组路由 (Group Routes):组织不改变 URL

用圆括号声明分组目录,分组名只用于组织代码,不会出现在 URL 中:

app/ (auth)/ login.tsx # URL: /login register.tsx # URL: /register (main)/ index.tsx # URL: / settings.tsx # URL: /settings

分组的三个典型用途:

  1. 组织相关路由:把登录/注册归入(auth),把主界面归入(main),让目录结构直接反映业务域;
  2. 应用不同布局:每个分组都可以有自己的_layout.tsx,从而对整组路由施加不同的导航容器或配置;
  3. 保持 URL 简洁:URL 中不会出现(auth)(main)这类实现细节。

五、Stacks 与 Tabs 的嵌套结构:Tab 内的独立导航栈

当应用使用 Tabs 时,header 与页面标题必须设置在嵌套在每个 Tab 内部的 Stack 中。这样每个 Tab 都拥有独立的导航历史和各自的 header;而根布局通常不显示 header。

核心操作要点:

  • 在 Tab 布局上把headerShown设为false
  • 使用(group)路由简化公开 URL;
  • 结构调整时,可能需要删除或重构已有的路由文件以适配新结构。

标准结构示例:

app/ _layout.tsx — <Tabs /> (home)/ _layout.tsx — <Stack /> index.tsx — <ScrollView /> (settings)/ _layout.tsx — <Stack /> index.tsx — <ScrollView /> (home,settings)/ info.tsx — <ScrollView /> (shared across tabs)

这里(home,settings)/info.tsx是跨 Tab 共享的页面——它同时隶属于 home 与 settings 两个分组,因此两个 Tab 都能 push 到它。

5.1 为什么 ScrollView 必须是第一个子组件

route-structure.md 的示例中每个页面都是<ScrollView />,这与 SKILL.md 的行为规范一致:

  • 路由属于 Stack 时,其第一个子组件几乎总是带contentInsetAdjustmentBehavior="automatic"ScrollView
  • 为响应式考虑,根组件一律包在滚动视图中,用contentInsetAdjustmentBehavior="automatic"取代<SafeAreaView>,以获得更智能的安全区适配;
  • FlatListSectionList同样应设置contentInsetAdjustmentBehavior="automatic"

这样从 tab 切换到页面内容时,滚动与安全区行为都保持原生观感。

六、Array Routes:多栈共享屏幕的高级布局

'(index,settings)'形式的 Array Route 用于创建多个 Stack,特别适合需要跨栈共享屏幕的 Tab 应用:

app/ _layout.tsx — <Tabs /> (index,settings)/ _layout.tsx — <Stack /> index.tsx — <ScrollView /> settings.tsx — <ScrollView />

它需要一个带显式 anchor 路由的专用布局:

// app/(index,settings)/_layout.tsx import { useMemo } from "react"; import Stack from "expo-router/stack"; export const unstable_settings = { index: { anchor: "index" }, settings: { anchor: "settings" }, }; export default function Layout({ segment }: { segment: string }) { const screen = segment.match(/\((.*)\)/)?.[1]!; const options = useMemo(() => { switch (screen) { case "index": return { headerRight: () => <></> }; default: return {}; } }, [screen]); return ( <Stack> <Stack.Screen name={screen} options={options} /> </Stack> ); }

关键机制解读:

  • Layout接收的segmentprop 就是当前激活的分组名(如(index)),通过segment.match(/\((.*)\)/)?.[1]!提取出indexsettings
  • 布局根据当前激活的屏幕动态渲染对应的Stack.Screen,因此两个 Tab 共享同一个 Stack 容器,可自由 push 公共详情页;
  • unstable_settings中为每个入口指定anchor(v4 中取代了initialRouteName),确保每个 Tab 都有明确的初始路由。

6.1 完整的生产级 App 结构示例

结合动态路由、Array Routes 与目录分离,一份完整结构如下:

app/ _layout.tsx — <NativeTabs /> (index,search)/ _layout.tsx — <Stack /> index.tsx — Main list search.tsx — Search view i/[id].tsx — Detail page components/ theme.tsx list.tsx utils/ storage.ts use-search.ts

注意components/utils/app/平级——这正是“绝不把组件与工具放进 app 目录”约定的落地形态。i/[id].tsx展示了 Array Route 内部同样可以使用动态段来承载详情页。

七、布局文件(Layout Files):每个目录的导航容器

每个目录都可以有一个_layout.tsx,用于包裹该目录下的全部路由:

// app/_layout.tsx import { Stack } from "expo-router/stack"; export default function RootLayout() { return <Stack />; }
// app/(tabs)/_layout.tsx import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs"; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> <Icon sf="house.fill" /> </NativeTabs.Trigger> </NativeTabs> ); }

7.1 与 NativeTabs 的组合(SDK 54/55)

第二个示例中的NativeTabs来自 tabs.md 推荐的expo-router/unstable-native-tabs。SDK 55+ 的推荐写法是组件化 API:

import { NativeTabs } from "expo-router/unstable-native-tabs"; export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

需要注意的规则(来自 tabs.md):

  • 每个 Tab 都必须有一个 Trigger,且NativeTabs.Triggername必须与路由名完全一致(包括圆括号,例如<NativeTabs.Trigger name="(search)">);
  • NativeTabs 不渲染 header,必须在每个 Tab 内部嵌套 Stack 来提供导航 header;
  • Tabs 必须是静态的——运行时动态增删 Tab 会重挂载导航器并丢失状态;
  • 搜索类 Tab 建议放在最后,便于与搜索栏结合。

7.2 与 JS Tabs 的差异速查

JS TabsNative Tabs
<Tabs.Screen><NativeTabs.Trigger>
options={{ title }}<NativeTabs.Trigger.Label>
options={{ tabBarIcon }}<NativeTabs.Trigger.Icon>
tabBarBadgeoption<NativeTabs.Trigger.Badge>
Props 驱动 API组件化 API
内置 header需嵌套<Stack>提供 header

八、路由设置(Route Settings):anchor 与 unstable_settings

通过导出unstable_settings来配置路由行为:

export const unstable_settings = { anchor: "index", };

重要变更:在 Expo Router v4 中,initialRouteName被重命名为anchor。在 Array Routes 中,每个入口都可通过unstable_settings独立指定自己的 anchor 路由(见第六节的index: { anchor: "index" }),这是多栈共享布局能够正确初始化的关键。

九、404 处理:+not-found.tsx

创建+not-found.tsx文件处理所有未匹配的路由:

// app/+not-found.tsx import { Link } from "expo-router"; import { View, Text } from "react-native"; export default function NotFound() { return ( <View> <Text>Page not found</Text> <Link href="/">Go home</Link> </View> ); }

该文件是 Expo Router 约定俗成的“兜底路由”——任何没有对应文件的 URL 都会落到这里,配合第一节的“根路由兜底”约定,应用永远不会出现空白页或死链。

十、实战要点汇总:把路由结构落到真实页面

10.1 页面标题放在 Stack 而非页面文本

SKILL.md 明确要求:一律使用导航栈标题(Stack title)而不是页面内的自定义文本元素。在 Stack 布局中通过Stack.Screen的 options 设置:

<Stack.Screen options={{ title: "Home" }} />

10.2 在 Stack 布局中统一配置 header

结合 Array Route 与useLocalSearchParams的实际需求,一个带搜索与透明 header 的共享 Stack 布局通常写成:

// app/(index,search)/_layout.tsx import { Stack } from "expo-router/stack"; import { colors } from "@/theme/colors"; export default function Layout({ segment }) { const screen = segment.match(/\((.*)\)/)?.[1]!; const titles: Record<string, string> = { index: "Items", search: "Search" }; return ( <Stack screenOptions={{ headerTransparent: true, headerShadowVisible: false, headerLargeTitle: true, headerTitleStyle: { color: colors.label }, headerBackButtonDisplayMode: "minimal", }} > <Stack.Screen name={screen} options={{ title: titles[screen] }} /> <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} /> </Stack> ); }

10.3 搜索页与查询参数的配合

路由结构规划好后,搜索栏可以直接挂在 Stack header 上(详见 search.md):

<Stack.Screen name="index" options={{ headerSearchBarOptions: { placeholder: "Search", onChangeText: (event) => console.log(event.nativeEvent.text), }, }} />

若搜索 Tab 使用role="search"的 NativeTabs,搜索栏会与 Tab 栏无缝集成,配合useLocalSearchParams即可实现“列表 → 详情”的完整检索闭环。

10.4 自定义 header 工具栏(iOS,SDK 55+)

对于需要 header 按钮的页面,Stack.Toolbar提供原生 iOS 工具栏(详见 toolbar-and-headers.md),它作为 Stack 的兄弟节点书写在页面组件内:

<> {/* ScrollView 必须是屏幕的第一个子组件 */} <ScrollView style={{ flex: 1 }} contentInsetAdjustmentBehavior="automatic"> {/* Screen content */} </ScrollView> <Stack.Screen.Title large>Folders</Stack.Screen.Title> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button icon="folder.badge.plus" onPress={() => {}} /> <Stack.Toolbar.Button onPress={() => {}}>Edit</Stack.Toolbar.Button> </Stack.Toolbar> </>

placement支持"left"(header 左侧)、"right"(header 右侧)与"bottom"(底部工具栏,默认值);注意placement="bottom"只能在屏幕组件内使用,不能写在布局文件中。

十一、常见误区与规避建议

结合 route-structure.md 与 SKILL.md,以下是实践中最容易踩的坑:

  1. app目录混放组件/工具:这是最严重的 anti-pattern,会导致路由扫描到非页面文件而报错或行为异常;组件进components/,工具进utils/
  2. (foo).tsx文件命名:圆括号是分组保留语法,单个文件不能直接叫(foo).tsx,请用(foo)/index.tsx
  3. 忘记根路由app下没有匹配/的路由时应用为空白页,可将 index 放入某个分组内解决。
  4. Trigger name 不匹配:NativeTabs 中name必须与路由名完全一致(含圆括号),如(search)
  5. 重构不删旧路由:移动或重构导航时务必同步删除旧路由文件,否则会被意外匹配。
  6. JS Tab 与 Native Tab 混用:NativeTabs 不渲染 header,需要 header 就在每个 Tab 内嵌套 Stack,并在 Tab 布局关闭headerShown

结语

Expo Router 的app目录即路由,掌握文件约定、动态段、分组与 Array Routes,就等于掌握了整个导航体系的设计语言。本文内容以 route-structure.md 为骨架,并补充了 SKILL.md、tabs.md、search.md、toolbar-and-headers.md 中的配套约定。实际编码时,请以官方文档为准核对版本差异(尤其是 v4 中initialRouteNameanchor的更名,以及 SDK 54/55 之间 NativeTabs API 的差异),并始终遵循“ScrollView 作为首个子组件、Stack title 承载标题、组件与工具隔离在 app 之外”这三条核心纪律,即可构建出结构清晰、体验原生的 Expo 应用。

【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,400+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询