Expo Router vs 原生React Native 完全对比指南

本文档详细对比了Expo项目与原生React Native项目在目录结构、路由机制、Tab导航等方面的核心差异。

📋 目录


📁 目录结构对比

原生React Native项目 (使用 react-native init 创建)

复制代码
my-app/
├── android/              ⚠️ 原生Android代码目录
│   ├── app/
│   ├── gradle/
│   └── build.gradle
├── ios/                  ⚠️ 原生iOS代码目录
│   ├── MyApp/
│   ├── MyApp.xcodeproj
│   └── Podfile
├── src/                  
│   ├── screens/
│   ├── components/
│   └── navigation/       ⚠️ 需要手动配置路由
├── App.tsx
├── index.js              ⚠️ 入口文件
├── metro.config.js
├── babel.config.js
└── package.json

Expo项目 (使用 npx create-expo-app 创建)

复制代码
my-app/
├── app/                  ✨ 基于文件的路由目录(pages)
│   ├── (tabs)/          ✨ 分组路由(Tab导航)
│   │   ├── _layout.tsx  ✨ Tab布局配置
│   │   ├── index.tsx    → 路由: /
│   │   └── explore.tsx  → 路由: /explore
│   ├── _layout.tsx      ✨ 根布局
│   ├── modal.tsx        → 路由: /modal
│   └── pages/user/
│       └── index.tsx    → 路由: /pages/user
│
├── components/          ✨ 可复用UI组件(非路由)
├── constants/
├── hooks/
├── assets/
├── app.json             ✨ Expo配置文件
├── babel.config.js
└── package.json

🔑 核心区别详解

1. 原生代码目录

特性 原生RN Expo
android/ ✅ 包含完整Android原生代码 ❌ 默认不包含(托管)
ios/ ✅ 包含完整iOS原生代码 ❌ 默认不包含(托管)
原生模块 ✅ 完全自由添加 ⚠️ 受限于Expo SDK
配置复杂度 🔴 高(需配置Gradle/Podfile) 🟢 低(app.json统一管理)
弹出选项 - ✅ 可用 expo prebuild 生成原生目录

说明

  • Expo默认使用托管工作流(Managed Workflow),无需接触原生代码
  • 如需使用Expo不支持的原生模块,运行 npx expo prebuild 转为裸工作流(Bare Workflow)

2. 路由系统

tsx 复制代码
// src/navigation/AppNavigator.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export default function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="User" component={UserScreen} />
        <Stack.Screen name="Modal" component={ModalScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

使用路由

tsx 复制代码
// 需要手动导航
navigation.navigate('User');
Expo - 文件系统路由(Expo Router)
复制代码
app/
├── index.tsx           → 自动映射路由: /
├── user.tsx            → 自动映射路由: /user
└── modal.tsx           → 自动映射路由: /modal

使用路由

tsx 复制代码
import { useRouter } from 'expo-router';

const router = useRouter();
router.push('/user');  // 直接使用路径

对比表格

特性 原生RN Expo Router
路由配置 手动在代码中注册 文件结构即路由
新增页面 1. 创建组件 2. 注册路由 3. 配置导航 1. 创建文件 ✅
路由跳转 navigation.navigate('ScreenName') router.push('/path')
类型安全 ⚠️ 需手动配置TypeScript ✅ 自动生成类型
URL支持 ❌ 需额外配置 ✅ 原生支持

3. 入口文件

原生RN
js 复制代码
// index.js
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('MyApp', () => App);
Expo
json 复制代码
// package.json
{
  "main": "expo-router/entry"
}

说明:Expo使用统一的入口,自动处理路由初始化。

4. 配置文件

原生RN - 多文件配置
复制代码
android/app/build.gradle    # Android配置
ios/Info.plist              # iOS配置
app.json                    # Metro配置
Expo - 统一配置
json 复制代码
// app.json
{
  "expo": {
    "name": "my-app",
    "icon": "./assets/icon.png",
    "android": { ... },
    "ios": { ... },
    "web": { ... }
  }
}

5. 原生模块添加流程

原生RN
bash 复制代码
# 1. 安装库
npm install react-native-camera

# 2. 链接原生代码(如需要)
cd ios && pod install

# 3. 手动配置
# - 修改 android/app/build.gradle
# - 修改 AndroidManifest.xml
# - 配置权限等
Expo
bash 复制代码
# Managed Workflow(仅Expo SDK模块)
npx expo install expo-camera

# Bare Workflow(任何模块)
npx expo prebuild  # 生成原生目录
npm install react-native-camera
cd ios && pod install

🎯 路由系统深度对比

核心理念差异

原生RN:组件即路由

  • 路由是运行时的导航配置
  • 需要显式声明每个Screen

Expo Router:文件即路由

  • 路由是编译时的文件结构
  • 文件系统映射为URL结构

实际案例:添加用户详情页

原生RN需要做的事
tsx 复制代码
// 1️⃣ 创建 src/screens/UserDetailScreen.tsx
export default function UserDetailScreen({ route }) {
  const { userId } = route.params;
  return <Text>User {userId}</Text>;
}

// 2️⃣ 修改 src/navigation/AppNavigator.tsx
<Stack.Screen 
  name="UserDetail" 
  component={UserDetailScreen} 
  options={{ title: '用户详情' }}
/>

// 3️⃣ 使用导航
navigation.navigate('UserDetail', { userId: 123 });
Expo Router需要做的事
tsx 复制代码
// 1️⃣ 创建 app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';

export default function UserDetailScreen() {
  const { id } = useLocalSearchParams();
  return <Text>User {id}</Text>;
}

// 2️⃣ 完成!使用路由
router.push('/user/123');

工作量对比

  • 原生RN:3个步骤,涉及2个文件
  • Expo Router:1个步骤,1个文件 ✅

🎨 Tab导航机制详解

原生RN的Tab实现

tsx 复制代码
// 需要创建 src/navigation/TabNavigator.tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/Ionicons';

const Tab = createBottomTabNavigator();

export default function TabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarActiveTintColor: '#007AFF',
        headerShown: false,
      }}>
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="home" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Explore"
        component={ExploreScreen}
        options={{
          tabBarIcon: ({ color, size }) => (
            <Icon name="compass" size={size} color={color} />
          ),
        }}
      />
    </Tab.Navigator>
  );
}

// 然后在主导航器中嵌套
<Stack.Navigator>
  <Stack.Screen name="MainTabs" component={TabNavigator} />
  <Stack.Screen name="Modal" component={ModalScreen} />
</Stack.Navigator>

Expo Router的Tab实现

目录结构
复制代码
app/
├── (tabs)/                 ✨ 括号=分组路由(不影响URL)
│   ├── _layout.tsx        ✨ Tab配置文件
│   ├── index.tsx          → URL: /
│   └── explore.tsx        → URL: /explore
├── _layout.tsx            ✨ 根布局
└── modal.tsx              → URL: /modal
Tab配置文件
tsx 复制代码
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { IconSymbol } from '@/components/ui/icon-symbol';

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#007AFF',
        headerShown: false,
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="house.fill" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="paperplane.fill" color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

分组路由 (tabs) 的魔法

关键特性

  1. 括号包裹的目录不会出现在URL中

    复制代码
    app/(tabs)/index.tsx  → URL: /        (不是 /tabs/)
    app/(tabs)/explore.tsx → URL: /explore (不是 /tabs/explore)
  2. _layout.tsx 决定子页面的渲染方式

    • 使用 <Tabs> → 底部Tab导航
    • 使用 <Stack> → 堆栈导航
    • 使用 <Drawer> → 抽屉导航
  3. 自动子页面注册

    • (tabs) 目录下的所有页面自动成为Tab页
    • 无需手动逐个注册

Tab底层封装链

复制代码
expo-router 的 <Tabs>
    ↓ 封装
@react-navigation/bottom-tabs 
    ↓ 基于
React Navigation 7.x
    ↓ 使用
React Native 核心组件 (View, TouchableOpacity, Animated)

添加新Tab页面对比

原生RN
tsx 复制代码
// 1️⃣ 创建 src/screens/ProfileScreen.tsx
export default function ProfileScreen() {
  return <Text>Profile</Text>;
}

// 2️⃣ 修改 TabNavigator.tsx
<Tab.Screen
  name="Profile"
  component={ProfileScreen}
  options={{
    tabBarIcon: ({ color, size }) => (
      <Icon name="person" size={size} color={color} />
    ),
  }}
/>
Expo Router
tsx 复制代码
// 1️⃣ 创建 app/(tabs)/profile.tsx
export default function ProfileScreen() {
  return <Text>Profile</Text>;
}

// 2️⃣ 修改 app/(tabs)/_layout.tsx
<Tabs.Screen
  name="profile"
  options={{
    title: 'Profile',
    tabBarIcon: ({ color }) => (
      <IconSymbol size={28} name="person.fill" color={color} />
    ),
  }}
/>

对比结果

  • 代码量相当
  • Expo Router更符合直觉(文件结构即路由)
  • URL访问:Expo自动支持 /profile 路径

📂 项目结构最佳实践

❌ 错误做法:所有代码放在 app/

复制代码
app/
├── index.tsx
├── user.tsx
├── api.ts              ❌ 会被识别为路由 /api
├── utils.ts            ❌ 会被识别为路由 /utils
└── config.ts           ❌ 会被识别为路由 /config

✅ 正确做法:app/ 只放页面

复制代码
my-app/
├── app/                  ✅ 纯粹的路由/页面
│   ├── (tabs)/
│   │   ├── _layout.tsx
│   │   ├── index.tsx
│   │   └── explore.tsx
│   ├── _layout.tsx
│   └── pages/user/
│       └── index.tsx
│
├── api/                  ✅ API调用层
│   ├── user.ts
│   ├── auth.ts
│   └── request.ts
│
├── utils/                ✅ 工具函数
│   ├── format.ts
│   ├── validate.ts
│   └── storage.ts
│
├── services/             ✅ 业务逻辑服务
│   ├── authService.ts
│   └── userService.ts
│
├── hooks/                ✅ 自定义Hooks
│   ├── useAuth.ts
│   └── useUser.ts
│
├── store/                ✅ 状态管理
│   ├── userStore.ts
│   └── appStore.ts
│
├── types/                ✅ TypeScript类型
│   ├── user.ts
│   └── api.ts
│
├── components/           ✅ UI组件(非路由)
│   ├── ui/
│   └── business/
│
├── constants/
└── assets/

核心原则

app/ 目录应该放的内容

  • ✅ 页面组件(index.tsx, about.tsx等)
  • ✅ 布局文件(_layout.tsx)
  • ✅ 特殊文件(+html.tsx, +not-found.tsx等)

app/ 目录不应该放的内容

  • ❌ API调用函数
  • ❌ 工具函数
  • ❌ 业务逻辑
  • ❌ 状态管理
  • ❌ 类型定义
  • ❌ 配置文件

原因app/ 目录下的文件/文件夹会被expo-router自动解析为路由!


💼 实战示例:完整的用户登录功能

原生RN实现

复制代码
src/
├── screens/
│   └── LoginScreen.tsx          # 页面
├── navigation/
│   └── AppNavigator.tsx         # 路由配置
├── api/
│   └── auth.ts                  # API调用
├── services/
│   └── AuthService.ts           # 业务逻辑
└── hooks/
    └── useAuth.ts               # Hook
tsx 复制代码
// 1️⃣ src/api/auth.ts
export const login = async (username: string, password: string) => {
  return await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ username, password }),
  });
};

// 2️⃣ src/services/AuthService.ts
export class AuthService {
  async login(username: string, password: string) {
    const data = await login(username, password);
    await AsyncStorage.setItem('token', data.token);
    return data;
  }
}

// 3️⃣ src/hooks/useAuth.ts
export const useAuth = () => {
  const [user, setUser] = useState(null);
  
  const handleLogin = async (username: string, password: string) => {
    const authService = new AuthService();
    const userData = await authService.login(username, password);
    setUser(userData);
  };
  
  return { user, login: handleLogin };
};

// 4️⃣ src/screens/LoginScreen.tsx
export default function LoginScreen({ navigation }) {
  const { login } = useAuth();
  
  const handleSubmit = async () => {
    await login(username, password);
    navigation.navigate('Home');  // 手动导航
  };
  
  return <View>...</View>;
}

// 5️⃣ src/navigation/AppNavigator.tsx
<Stack.Screen name="Login" component={LoginScreen} />

Expo Router实现

复制代码
my-app/
├── app/
│   └── login.tsx                # 页面(自动路由)
├── api/
│   └── auth.ts                  # API调用
├── services/
│   └── authService.ts           # 业务逻辑
└── hooks/
    └── useAuth.ts               # Hook
tsx 复制代码
// 1️⃣ api/auth.ts(同上)
export const login = async (username: string, password: string) => {
  return await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ username, password }),
  });
};

// 2️⃣ services/authService.ts(同上)
export class AuthService {
  async login(username: string, password: string) {
    const data = await login(username, password);
    await AsyncStorage.setItem('token', data.token);
    return data;
  }
}

// 3️⃣ hooks/useAuth.ts(同上)
export const useAuth = () => {
  const [user, setUser] = useState(null);
  
  const handleLogin = async (username: string, password: string) => {
    const authService = new AuthService();
    const userData = await authService.login(username, password);
    setUser(userData);
  };
  
  return { user, login: handleLogin };
};

// 4️⃣ app/login.tsx
import { useRouter } from 'expo-router';

export default function LoginScreen() {
  const { login } = useAuth();
  const router = useRouter();
  
  const handleSubmit = async () => {
    await login(username, password);
    router.replace('/');  // 使用路径导航
  };
  
  return <View>...</View>;
}

// ✅ 无需第5步!文件即路由

关键差异

  1. 路由注册:Expo Router无需手动注册
  2. 导航方式
    • 原生RN:navigation.navigate('ScreenName')
    • Expo:router.push('/path')
  3. 类型安全:Expo自动生成路由类型

📊 优劣势总结

原生React Native

优势

  • ✅ 完全掌控原生代码
  • ✅ 不受第三方SDK限制
  • ✅ 可使用任何原生模块
  • ✅ 更成熟的生态系统

劣势

  • ❌ 配置复杂(Gradle、Podfile等)
  • ❌ 需要Android Studio/Xcode
  • ❌ 路由配置繁琐
  • ❌ 学习曲线陡峭
  • ❌ 构建时间长

Expo + Expo Router

优势

  • ✅ 开发速度极快
  • ✅ 文件系统路由(类似Next.js)
  • ✅ 无需接触原生代码
  • ✅ 统一配置管理(app.json)
  • ✅ 内置OTA更新
  • ✅ 云构建支持(EAS Build)
  • ✅ Web支持开箱即用
  • ✅ 学习曲线平缓

劣势

  • ❌ 原生模块受限(Managed Workflow)
  • ⚠️ 应用体积稍大
  • ⚠️ 需要Expo SDK支持的功能

弹出选项

bash 复制代码
# 需要使用Expo不支持的原生模块时
npx expo prebuild

# 生成 android/ 和 ios/ 目录
# 转为 Bare Workflow,获得原生RN的灵活性

🎓 学习路径建议

新手推荐

  1. 从Expo开始

    • 快速上手React Native开发
    • 理解组件、状态、导航等核心概念
    • 体验文件系统路由的便利
  2. 掌握Expo Router

    • 文件结构即路由
    • 布局系统(_layout.tsx)
    • 分组路由(括号语法)
    • 动态路由([id].tsx)
  3. 深入原生RN(可选)

    • 需要特殊原生模块时
    • 学习React Navigation
    • 理解原生桥接机制

项目选择建议

使用Expo如果

  • 快速原型开发
  • 创业公司MVP
  • 大部分功能Expo SDK覆盖
  • 团队没有原生开发经验

使用原生RN如果

  • 需要大量自定义原生模块
  • 对包体积有严格要求
  • 需要深度定制原生功能
  • 团队有原生开发能力

📚 参考资源

官方文档

关键概念

  • 文件系统路由:文件结构映射为URL路由
  • 分组路由 :使用括号 (name) 组织路由,不影响URL
  • 布局路由_layout.tsx 控制子页面渲染方式
  • 动态路由[param].tsx 创建动态路径参数

🔄 迁移指南

从原生RN迁移到Expo

  1. 安装Expo

    bash 复制代码
    npx install-expo-modules@latest
  2. 重构路由结构

    复制代码
    src/screens/  →  app/
    src/navigation/  →  删除(使用文件路由)
  3. 更新导航代码

    tsx 复制代码
    // 之前
    navigation.navigate('User', { id: 123 });
    
    // 之后
    router.push('/user/123');
  4. 配置文件迁移

    复制代码
    android/  →  可选(使用 expo prebuild 生成)
    ios/      →  可选(使用 expo prebuild 生成)

💡 最佳实践总结

  1. app/ 目录只放页面

    • 相当于Next.js的 pages/ 或 app/
    • 不要放业务逻辑、工具函数等
  2. 业务代码同级组织

    • api/ - API调用
    • services/ - 业务逻辑
    • utils/ - 工具函数
    • hooks/ - 自定义Hooks
    • store/ - 状态管理
  3. 善用分组路由

    • (tabs)/ - Tab导航
    • (auth)/ - 认证相关页面
    • (drawer)/ - 抽屉导航
  4. 类型安全

    • Expo Router自动生成路由类型
    • 使用 Href<> 类型获得路径提示
  5. 渐进增强

    • 从Managed Workflow开始
    • 需要时使用 expo prebuild 转为Bare Workflow

🎉 结语

Expo + Expo Router 代表了React Native开发的现代化方向

  • 📁 文件系统路由 - 告别繁琐的路由配置
  • 🚀 快速迭代 - 专注业务逻辑而非基础设施
  • 🌐 Web优先 - 天然支持跨平台(含Web)
  • 🛠️ 渐进增强 - 需要时可转为原生RN

无论选择哪种方案,理解它们的差异和适用场景才是关键!


文档创建时间:2026-01-15
基于 Expo 54.0 / expo-router 6.0

相关推荐
桃子叔叔1 小时前
react-wavesurfer录音组件2:前端如何处理后端返回的仅Blob字段
前端·react.js·状态模式
2501_948194982 小时前
RN for OpenHarmony AnimeHub项目实战:历史记录页面开发
react native
烧饼Fighting2 小时前
统信UOS操作系统离线安装ffmpeg
开发语言·javascript·ffmpeg
m0_748254662 小时前
Angular 2 模板语法概述
前端·javascript·angular.js
小小前端--可笑可笑2 小时前
【Three.js + MediaPipe】视频粒子特效:实时运动检测与人物分割技术详解
开发语言·前端·javascript·音视频·粒子特效
摘星编程2 小时前
React Native for OpenHarmony 实战:Keyboard 键盘事件详解
react native·react.js·计算机外设
奔跑的web.2 小时前
JavaScript 对象属性遍历Object.entries Object.keys:6 种常用方法详解与对比
开发语言·前端·javascript·vue.js
摘星编程2 小时前
React Native for OpenHarmony 实战:ToastAndroid 安卓提示详解
android·react native·react.js