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

前言

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

项目结构目录如下

csharp 复制代码
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]

javascript 复制代码
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]
php 复制代码
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]
javascript 复制代码
// 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]

javascript 复制代码
同上[/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 仅做路径分组,不额外生成导航层级

效果图

总结

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

相关推荐
微客鸟窝4 小时前
ethers.js 开发的核心场景
web3
dingzd955 小时前
如何轻松解除Facebook封锁
web3·互联网·facebook·tiktok·instagram·指纹浏览器·clonbrowser
清 晨5 小时前
创建多个Facebook账号的终极解决方案
web3·互联网·facebook·tiktok·instagram·指纹浏览器·clonbrowser
iOS阿玮10 小时前
苹果审核被拒要听劝,能沟通回复解决真的不用改!
uni-app·app·apple
pe7er19 小时前
React Native 多环境配置全攻略:环境变量、iOS Scheme 和 Android Build Variant
前端·react native·react.js
iOS阿玮1 天前
成年人的沟通,不谈钱谈什么?谈感情?
uni-app·app·apple
iOS阿玮2 天前
纯粹的广告变现,已经来到了山穷水尽的地步
uni-app·app·apple
清 晨3 天前
Web3.0引领互联网未来,助力安全防护升级
安全·web3·互联网·facebook·tiktok·instagram·clonbrowser
运维开发王义杰3 天前
Web3: 用ERC-1400革新公司股权激励
web3