React Native DApp 开发全栈实战·从 0 到 1 系列(expo-router)
项目结构目录如下
RnDApp/ ├── android/ # Android 原生工程(Expo Prebuild 后生成) ├── api/ # 后端接口封装层 ├── app/ # Expo Router 路由目录 │ ├── (tabs)/ # 底部 Tab 路由组 │ │ ├── discover/ # /discover │ │ ├── home/ # /home │ │ ├── my/ # /my │ │ ├── swap/ # /swap │ │ └── trade/ # /trade │ │ └── layout.tsx # Tab 布局 │ ├── profile/ # 独立路由组 │ │ ├── layout.tsx │ │ └── +not-found.tsx │ ├── createAccount.tsx # /createAccount │ ├── createWallet.tsx # /createWallet │ ├── index.tsx # 首页 / │ ├── login.tsx # /login │ └── register.tsx # /register ├── assets/ # 图片、字体、音视频等静态资源 ├── components/ # 公共业务组件 ├── constants/ # 枚举、常量、主题配置 ├── hooks/ # 自定义 React Hooks ├── node_modules/ # 依赖包 ├── scripts/ # 构建、自动化脚本 ├── stores/ # 全局状态管理(Zustand / Redux) ├── .gitignore ├── app.json # Expo 项目配置 ├── babel.config.js ├── global.css # Babel 配置(NativeWind 等) ├── metro.config.js ├── package.json ├── tailwind.config.js └── tsconfig.json
项目说明
- 样式:Nativewind(Tailwind CSS 语法)
- 状态:TanStack Query(服务端)+ Zustand(客户端)
- 请求:Axios
- 路由:Expo Router
expo-router(文件路由)
说明:文件即路由,括号文件夹不生成路径,_layout.tsx 负责导航配置
分类描述
1. 文件配置[/app/_layout.tsx]
import { useColorScheme } from '@/hooks/useColorScheme'; import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import 'react-native-reanimated'; import "../global.css"; const queryClient = new QueryClient(); export default function RootLayout() { const colorScheme = useColorScheme(); const [loaded] = useFonts({ SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), }); if (!loaded) { // Async font loading only occurs in development. return null; } return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <QueryClientProvider client={queryClient}> <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="+not-found" /> </Stack> <StatusBar style="auto" /> </QueryClientProvider> </ThemeProvider> ); }
说明:配置了tanstack/react-query和tailwind以及路由配置:包含底部导航和入口文件以及未匹配路由页面
2. 底部导航配置[/app/tabs/_layout.tsx] & [/app/tabs/home/_layout.tsx]
- [/app/tabs/_layout.tsx]
import { HapticTab } from '@/components/HapticTab'; import { IconSymbol } from '@/components/ui/IconSymbol'; import TabBarBackground from '@/components/ui/TabBarBackground'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import FontAwesome6 from '@expo/vector-icons/FontAwesome6'; import Ionicons from '@expo/vector-icons/Ionicons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { CommonActions } from '@react-navigation/native'; import { Tabs } from 'expo-router'; import React from 'react'; import { Platform } from 'react-native'; export default function TabLayout() { const colorScheme = useColorScheme(); return ( <> <Tabs screenOptions={{ tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, headerShown: false, tabBarButton: HapticTab, tabBarBackground: TabBarBackground, tabBarStyle: Platform.select({ ios: { // Use a transparent background on iOS to show the blur effect position: 'absolute', }, default: {}, }), }}> <Tabs.Screen name="home" options={{ title: 'Home', tabBarLabel: () => null, tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />, }} listeners={({ navigation }) => ({ tabPress: (e) => { e.preventDefault(); // 阻止默认跳转 navigation.dispatch( CommonActions.reset({ index: 0, routes: [ { name: 'home', // 对应 my/index state: { routes: [{ name: 'index' }], index: 0, }, }, ], }) ); }, })} /> <Tabs.Screen name="trade" options={{ // title: 'Trade', tabBarLabel: () => null, tabBarIcon: ({ color, focused }) => ( <FontAwesome6 name="btc" size={24} color={color} /> ), }} listeners={({ navigation }) => ({ tabPress: (e) => { e.preventDefault(); // 阻止默认跳转 navigation.dispatch( CommonActions.reset({ index: 0, routes: [ { name: 'trade', // 对应 my/index state: { routes: [{ name: 'index' }], index: 0, }, }, ], }) ); }, })} /> <Tabs.Screen name="swap" options={{ // title: 'Trade', tabBarLabel: () => null, tabBarIcon: ({ color, focused }) => ( <MaterialIcons name="swap-horizontal-circle" size={24} color={color} /> ), }} listeners={({ navigation }) => ({ tabPress: (e) => { e.preventDefault(); // 阻止默认跳转 navigation.dispatch( CommonActions.reset({ index: 0, routes: [ { name: 'swap', // 对应 my/index state: { routes: [{ name: 'index' }], index: 0, }, }, ], }) ); }, })} /> <Tabs.Screen name="discover" options={{ tabBarLabel: () => null, // title: 'My', tabBarIcon: ({ color, focused }) => ( <Ionicons name="compass" size={24} color={color} /> ), }} listeners={({ navigation }) => ({ tabPress: (e) => { e.preventDefault(); // 阻止默认跳转 navigation.dispatch( CommonActions.reset({ index: 0, routes: [ { name: 'discover', // 对应 my/index state: { routes: [{ name: 'index' }], index: 0, }, }, ], }) ); }, })} /> <Tabs.Screen name="my" options={{ tabBarLabel: () => null, // title: 'My', tabBarIcon: ({ color, focused }) => ( <FontAwesome6 name="user-large" size={24} color={color} /> ), }} listeners={({ navigation }) => ({ tabPress: (e) => { e.preventDefault(); // 阻止默认跳转 navigation.dispatch( CommonActions.reset({ index: 0, routes: [ { name: 'my', // 对应 my/index state: { routes: [{ name: 'index' }], index: 0, }, }, ], }) ); }, })} /> </Tabs> </>); }
说明:listeners监听事件解决底部导航跳转默认页面(index)options主要配置导航的设置包含icon和文字
- [/app/tabs/home/_layout.tsx]
// app/(tabs)/home/_layout.tsx import { Stack } from 'expo-router'; export default function DiscoverStack() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" /> </Stack> ); }
说明:底部导航要配合使用,主要解决双导航问题
3. 文件夹不含导航配置[/app/xxx/_layout.tsx]
同上[/app/tabs/home/_layout.tsx]
汇总速查
| 分类 | 路径示例 | 对应路由 | 导航行为 |
|---|---|---|---|
| 独立页面 | /app/index.tsx |
/ |
无父级导航,直接渲染 |
| 独立页面 | /app/login.tsx |
/login |
同上 |
| 底部导航 | /app/(tabs)/home.tsx |
/home |
自动嵌套 Tab;由 /app/(tabs)/_layout.tsx 统一配置 |
| 底部导航 | /app/(tabs)/swap.tsx |
/swap |
同上 |
| 分组文件夹(无导航) | /app/profile/settings.tsx |
/profile/settings |
仅做路径分组,不额外生成导航层级 |
更多推荐

所有评论(0)