React Native DApp 开发全栈实战·从 0 到 1 系列(expo-router)

  • 木西
  • 发布于 2025-08-12 22:42
  • 阅读 331

前言基于上篇文章《ReactNativeDApp开发全栈实战·从0到1系列(开篇)》,本文聚焦ReactNative路由方案:从导航架构选型到实战落地,带你一次配好、随处复用;项目结构目录如下RnDApp/├──android/

前言

基于上篇文章《React Native DApp 开发全栈实战·从 0 到 1 系列(开篇)》,本文聚焦 React Native 路由方案:从导航架构选型到实战落地,带你一次配好、随处复用;

项目结构目录如下

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 (
    &lt;Stack screenOptions={{ headerShown: false }}>
      &lt;Stack.Screen name="index" />
    &lt;/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 仅做路径分组,不额外生成导航层级

效果图

<div style="display:flex; gap:8px;flex-wrap:wrap;width:100%"> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/fefbd707c44740329d9ab0efe0b89b51~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=jtfbcAalfgP%2FOlNcLx9neO8KjYI%3D" alt="图1" width="200" /> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/83899814b8ca42b9b8ac96c12a083cc4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=Fx5oHXFtZ9yJZcmu%2BpogbdF5qcw%3D" alt="图5转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6eaf7b0a8acf4214bd17de2410f98bd6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=TJwZsJ3xaHkXRv7OBYm5VSjdO%2F4%3D" alt="图3" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a9a8d3d49f384bf28fca954a57bdeccc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=pADEHgh96bKyj6A2ciW4Lcypp1c%3D" alt="图2" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/40a066a730144bc480340c88ff66691d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=D6OLA9ROKV6%2Ffgyrmughwzn3zgA%3D" alt="图6转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d42aad77527f406686371285a1ea7398~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=Bs5l7SeAClo%2F6v1LXkYBo5bUuKA%3D" alt="图7转存失败,建议直接上传图片文件" width="200"/> <img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2e2d3ae6b6ed47d2bb2aae7f6a80b081~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1755614043&x-orig-sign=FOHW0NK0ymyVd9BkTzfNnG8lxv0%3D" alt="图4转存失败,建议直接上传图片文件" width="200"/> </div>

总结

至此,导航系统配置已全部完成,以及项目的页面效果。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。