ReactNative导航(ExpoRouter)Skill rn-navigation

Expo Router 是一个用于 React Native 应用的文件系统基础路由库,支持导航、路由、深度链接、标签栏、模态框和导航状态管理。

移动开发 0 次安装 0 次浏览 更新于 3/3/2026

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)

  1. 在 app.json 中添加 associatedDomains
  2. 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