name: rn-navigation description: Expo Router 导航模式用于 React Native。在实现 Expo 应用中的导航、路由、深度链接、标签栏、模态框或处理导航状态时使用。
React Native 导航 (Expo Router)
文件系统基础路由
Expo Router 使用基于文件系统的路由。app/ 中的文件成为路由。
路由结构
app/
├── _layout.tsx # 根布局(提供者,全局 UI)
├── index.tsx # "/" 路由
├── (tabs)/ # 标签组(括号 = 布局组)
│ ├── _layout.tsx # 标签栏配置
│ ├── home.tsx # "/home" 标签
│ └── profile.tsx # "/profile" 标签
├── (auth)/ # 认证流程组
│ ├── _layout.tsx # 认证特定的布局
│ ├── login.tsx # "/login"
│ └── register.tsx # "/register"
├── settings/
│ ├── index.tsx # "/settings"
│ └── [id].tsx # "/settings/123" (动态)
└── [...missing].tsx # 捕获所有 404
布局组 (groupName)
括号创建布局组 - 它们影响布局层次结构,但不会影响 URL:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: '首页',
tabBarIcon: ({ color }) => <HomeIcon color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{ title: '个人资料' }}
/>
</Tabs>
);
}
动态路由 [param]
// app/player/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function PlayerScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <PlayerProfile playerId={id} />;
}
捕获所有路由 [...slug]
// app/[...missing].tsx
import { Link, Stack } from 'expo-router';
export default function NotFound() {
return (
<>
<Stack.Screen options={{ title: '未找到' }} />
<Link href="/">返回首页</Link>
</>
);
}
导航模式
程序化导航
import { useRouter, Link } from 'expo-router';
function MyComponent() {
const router = useRouter();
// 使用 push 导航(添加到堆栈)
router.push('/player/123');
// 使用参数导航
router.push({
pathname: '/player/[id]',
params: { id: '123' },
});
// 替换当前屏幕(无返回)
router.replace('/home');
// 返回上一步
router.back();
// 导航到根目录
router.navigate('/');
// 取消模态框
router.dismiss();
}
链接组件
import { Link } from 'expo-router';
// 简单链接
<Link href="/settings">设置</Link>
// 带参数
<Link href={{ pathname: '/player/[id]', params: { id: '123' } }}>
查看玩家
</Link>
// 作为按钮
<Link href="/schedule" asChild>
<Pressable>
<Text>查看日程</Text>
</Pressable>
</Link>
// 替换而不是推送
<Link href="/home" replace>首页</Link>
堆栈导航
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: 'modal' }}
/>
<Stack.Screen
name="player/[id]"
options={{
headerTitle: '球员',
headerBackTitle: '返回',
}}
/>
</Stack>
);
}
动态头部选项
// app/player/[id].tsx
import { Stack, useLocalSearchParams } from 'expo-router';
export default function PlayerScreen() {
const { id } = useLocalSearchParams();
const player = usePlayer(id);
return (
<>
<Stack.Screen
options={{
headerTitle: player?.name ?? '加载中...',
headerRight: () => (
<EditButton playerId={id} />
),
}}
/>
<PlayerProfile player={player} />
</>
);
}
模态框
// 从任何地方呈现为模态框
router.push('/booking-modal');
// app/booking-modal.tsx
import { Stack, useRouter } from 'expo-router';
export default function BookingModal() {
const router = useRouter();
const handleComplete = () => {
router.dismiss(); // 或 router.back()
};
return (
<>
<Stack.Screen
options={{
presentation: 'modal',
headerLeft: () => (
<Button title="取消" onPress={() => router.dismiss()} />
),
}}
/>
<BookingForm onComplete={handleComplete} />
</>
);
}
// 在 _layout.tsx 中配置模态屏幕
<Stack.Screen
name="booking-modal"
options={{
presentation: 'modal',
headerShown: true,
}}
/>
深度链接
在 app.json 中配置方案
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.yourcompany.myapp",
"associatedDomains": ["applinks:yourdomain.com"]
}
}
}
测试深度链接
# iOS 模拟器
npx uri-scheme open "myapp://player/123" --ios
# 物理设备
npx expo start --dev-client
# 然后在 Safari 中打开 myapp://player/123
通用链接 (iOS)
- 在 app.json 中添加
associatedDomains - 在
https://yourdomain.com/.well-known/apple-app-site-association托管apple-app-site-association文件:
{
"applinks": {
"apps": [],
"details": [{
"appID": "TEAMID.com.yourcompany.myapp",
"paths": ["/player/*", "/schedule/*"]
}]
}
}
处理传入链接
// app/_layout.tsx
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
import { useRouter } from 'expo-router';
export default function RootLayout() {
const router = useRouter();
useEffect(() => {
// 处理打开应用的链接
Linking.getInitialURL().then((url) => {
if (url) handleDeepLink(url);
});
// 处理应用打开时的链接
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
function handleDeepLink(url: string) {
const { path, queryParams } = Linking.parse(url);
// Expo Router 自动处理大多数情况
// 特殊案例的自定义逻辑在这里
}
return <Stack />;
}
常见模式
受保护的认证路由
查看 rn-auth 技能以获取完整的认证上下文模式。关键导航部分:
// app/_layout.tsx
export default function RootLayout() {
const { token, isLoading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!token && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (token && inAuthGroup) {
router.replace('/(tabs)/home');
}
}, [token, isLoading]);
return (
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}
防止返回导航
// 登录成功后,替换以防止返回登录
router.replace('/(tabs)/home');
// 对于引导完成
router.replace('/home');
// 在屏幕选项中
<Stack.Screen
name="checkout-complete"
options={{
headerBackVisible: false,
gestureEnabled: false, // 防止滑动返回
}}
/>
在屏幕之间传递数据
// 选项 1: URL 参数(简单数据,刷新后仍然存在)
router.push({
pathname: '/confirm',
params: { date: '2025-01-15', courtId: '5' },
});
// 读取
const { date, courtId } = useLocalSearchParams();
// 选项 2: 全局状态用于复杂数据(刷新后不复存在)
// 使用上下文,zustand 或类似
调试导航
日志当前路由
import { usePathname, useSegments } from 'expo-router';
function DebugNav() {
const pathname = usePathname();
const segments = useSegments();
console.log('当前路径:', pathname);
console.log('段:', segments);
return null;
}
常见问题
| 问题 | 解决方案 |
|---|---|
| 屏幕未找到 | 检查文件名是否匹配路由,检查 _layout.tsx 是否包含屏幕 |
| 标签未显示 | 确保标签屏幕是标签 _layout.tsx 的直接子代 |
| 返回按钮缺失 | 检查父级和子级布局中的 headerShown |
| 深度链接不工作 | 验证 app.json 中的方案,使用 uri-scheme CLI 测试 |
| 参数未定义 | 使用 useLocalSearchParams 而不是 useSearchParams |