React Router 入门教程:构建记账应用

React Router 入门教程:构建记账应用

【免费下载链接】react-router 【免费下载链接】react-router 项目地址: https://gitcode.com/gh_mirrors/reac/react-router

前言:为什么选择React Router?

在现代前端开发中,单页面应用(SPA,Single Page Application)已成为主流。React Router作为React生态中最流行的路由解决方案,提供了强大的客户端路由功能,让你的应用能够:

  • ✅ 实现无刷新页面跳转
  • ✅ 管理复杂的嵌套路由结构
  • ✅ 处理数据加载和表单提交
  • ✅ 提供优雅的错误处理机制
  • ✅ 支持SEO友好的URL结构

本教程将通过构建一个完整的记账应用,带你从零开始掌握React Router的核心概念和使用技巧。

项目概述:记账应用功能规划

我们的记账应用将包含以下核心功能:

功能模块描述路由设计
仪表盘显示收支概览和统计/
交易列表查看所有交易记录/transactions
添加交易创建新的收入或支出/transactions/new
编辑交易修改现有交易信息/transactions/:id/edit
分类管理管理收支分类/categories
报表分析查看收支分析图表/reports

环境准备与项目初始化

技术栈选择

# 创建React项目
npm create vite@latest money-tracker -- --template react
cd money-tracker

# 安装依赖
npm install react-router-dom localforage lucide-react
npm install -D @types/node

项目目录结构

src/
├── components/          # 通用组件
├── routes/             # 路由组件
│   ├── Root.tsx        # 根布局
│   ├── Dashboard.tsx   # 仪表盘
│   ├── Transactions.tsx # 交易列表
│   └── ...
├── hooks/              # 自定义Hook
├── utils/              # 工具函数
├── types/              # TypeScript类型定义
└── main.tsx            # 应用入口

核心概念解析:React Router基础

1. 路由类型对比

路由类型适用场景特点
BrowserRouter现代Web应用使用HTML5 History API,URL美观
HashRouter静态文件部署使用URL hash,兼容性好
MemoryRouter测试环境路由状态保存在内存中

2. 路由配置方式

mermaid

3. 核心组件功能表

组件作用常用属性
<Link>导航链接to, state
<NavLink>活动状态导航className, style
<Outlet>子路由占位符-
<Navigate>编程式导航to, replace

实战开发:构建记账应用

步骤1:配置基础路由结构

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import './index.css'

// 导入路由组件
import Root from './routes/Root'
import Dashboard from './routes/Dashboard'
import Transactions from './routes/Transactions'
import NewTransaction from './routes/NewTransaction'
import EditTransaction from './routes/EditTransaction'
import Categories from './routes/Categories'
import Reports from './routes/Reports'
import ErrorPage from './routes/ErrorPage'

// 创建路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Dashboard />,
      },
      {
        path: 'transactions',
        element: <Transactions />,
      },
      {
        path: 'transactions/new',
        element: <NewTransaction />,
      },
      {
        path: 'transactions/:id/edit',
        element: <EditTransaction />,
      },
      {
        path: 'categories',
        element: <Categories />,
      },
      {
        path: 'reports',
        element: <Reports />,
      },
    ],
  },
])

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
)

步骤2:创建根布局组件

// src/routes/Root.tsx
import { Outlet, Link, useNavigation } from 'react-router-dom'
import { 
  Home, 
  List, 
  Plus, 
  PieChart, 
  Settings,
  Loader2 
} from 'lucide-react'

export default function Root() {
  const navigation = useNavigation()
  
  return (
    <div className="app-container">
      {/* 侧边栏导航 */}
      <aside className="sidebar">
        <div className="sidebar-header">
          <h1>💰 记账本</h1>
        </div>
        
        <nav className="sidebar-nav">
          <Link to="/" className="nav-item">
            <Home size={20} />
            <span>仪表盘</span>
          </Link>
          
          <Link to="/transactions" className="nav-item">
            <List size={20} />
            <span>交易记录</span>
          </Link>
          
          <Link to="/transactions/new" className="nav-item">
            <Plus size={20} />
            <span>添加交易</span>
          </Link>
          
          <Link to="/categories" className="nav-item">
            <Settings size={20} />
            <span>分类管理</span>
          </Link>
          
          <Link to="/reports" className="nav-item">
            <PieChart size={20} />
            <span>报表分析</span>
          </Link>
        </nav>
      </aside>

      {/* 主内容区域 */}
      <main className="main-content">
        {navigation.state === 'loading' && (
          <div className="loading-overlay">
            <Loader2 className="animate-spin" size={24} />
          </div>
        )}
        <Outlet />
      </main>
    </div>
  )
}

步骤3:实现数据加载与状态管理

// src/utils/storage.ts
import localforage from 'localforage'

// 初始化本地存储
export const transactionsDB = localforage.createInstance({
  name: 'money-tracker-transactions'
})

export const categoriesDB = localforage.createInstance({
  name: 'money-tracker-categories'
})

// 数据类型定义
export interface Transaction {
  id: string
  type: 'income' | 'expense'
  amount: number
  category: string
  description: string
  date: string
}

export interface Category {
  id: string
  name: string
  type: 'income' | 'expense'
  color: string
}

// 数据操作函数
export const getTransactions = async (): Promise<Transaction[]> => {
  return (await transactionsDB.getItem('transactions')) || []
}

export const saveTransaction = async (transaction: Omit<Transaction, 'id'>) => {
  const transactions = await getTransactions()
  const newTransaction: Transaction = {
    ...transaction,
    id: Date.now().toString()
  }
  transactions.push(newTransaction)
  await transactionsDB.setItem('transactions', transactions)
  return newTransaction
}

步骤4:实现交易列表页面

// src/routes/Transactions.tsx
import { 
  Link, 
  useLoaderData,
  useNavigation 
} from 'react-router-dom'
import { 
  Plus, 
  Edit, 
  Trash2, 
  ArrowUp, 
  ArrowDown 
} from 'lucide-react'
import { Transaction, getTransactions } from '../utils/storage'

export async function loader() {
  const transactions = await getTransactions()
  return { transactions }
}

export default function Transactions() {
  const { transactions } = useLoaderData() as { transactions: Transaction[] }
  const navigation = useNavigation()

  const totalIncome = transactions
    .filter(t => t.type === 'income')
    .reduce((sum, t) => sum + t.amount, 0)

  const totalExpense = transactions
    .filter(t => t.type === 'expense')
    .reduce((sum, t) => sum + t.amount, 0)

  return (
    <div className="page-container">
      <div className="page-header">
        <h2>交易记录</h2>
        <Link to="/transactions/new" className="btn btn-primary">
          <Plus size={16} />
          新增交易
        </Link>
      </div>

      {/* 统计卡片 */}
      <div className="stats-grid">
        <div className="stat-card income">
          <div className="stat-icon">
            <ArrowUp size={24} />
          </div>
          <div className="stat-content">
            <h3>总收入</h3>
            <p className="stat-amount">¥{totalIncome.toFixed(2)}</p>
          </div>
        </div>
        
        <div className="stat-card expense">
          <div className="stat-icon">
            <ArrowDown size={24} />
          </div>
          <div className="stat-content">
            <h3>总支出</h3>
            <p className="stat-amount">¥{totalExpense.toFixed(2)}</p>
          </div>
        </div>
        
        <div className="stat-card balance">
          <div className="stat-content">
            <h3>结余</h3>
            <p className="stat-amount">¥{(totalIncome - totalExpense).toFixed(2)}</p>
          </div>
        </div>
      </div>

      {/* 交易表格 */}
      <div className="card">
        <table className="data-table">
          <thead>
            <tr>
              <th>日期</th>
              <th>类型</th>
              <th>分类</th>
              <th>描述</th>
              <th>金额</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {transactions.map((transaction) => (
              <tr key={transaction.id}>
                <td>{new Date(transaction.date).toLocaleDateString()}</td>
                <td>
                  <span className={`badge ${transaction.type}`}>
                    {transaction.type === 'income' ? '收入' : '支出'}
                  </span>
                </td>
                <td>{transaction.category}</td>
                <td>{transaction.description}</td>
                <td className={transaction.type}>
                  {transaction.type === 'income' ? '+' : '-'}
                  ¥{transaction.amount.toFixed(2)}
                </td>
                <td>
                  <div className="action-buttons">
                    <Link
                      to={`/transactions/${transaction.id}/edit`}
                      className="btn-icon"
                    >
                      <Edit size={16} />
                    </Link>
                    <button className="btn-icon danger">
                      <Trash2 size={16} />
                    </button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        
        {transactions.length === 0 && (
          <div className="empty-state">
            <p>暂无交易记录</p>
            <Link to="/transactions/new" className="btn btn-primary">
              添加第一笔交易
            </Link>
          </div>
        )}
      </div>
    </div>
  )
}

步骤5:实现添加交易表单

// src/routes/NewTransaction.tsx
import { Form, redirect } from 'react-router-dom'
import { ArrowLeft } from 'lucide-react'
import { saveTransaction } from '../utils/storage'

export async function action({ request }: { request: Request }) {
  const formData = await request.formData()
  const transaction = {
    type: formData.get('type') as 'income' | 'expense',
    amount: parseFloat(formData.get('amount') as string),
    category: formData.get('category') as string,
    description: formData.get('description') as string,
    date: formData.get('date') as string,
  }
  
  await saveTransaction(transaction)
  return redirect('/transactions')
}

export default function NewTransaction() {
  return (
    <div className="page-container">
      <div className="page-header">
        <h2>添加交易</h2>
        <a href="/transactions" className="btn btn-secondary">
          <ArrowLeft size={16} />
          返回
        </a>
      </div>

      <div className="card">
        <Form method="post" className="transaction-form">
          <div className="form-grid">
            <div className="form-group">
              <label htmlFor="type">交易类型</label>
              <select id="type" name="type" required>
                <option value="">选择类型</option>
                <option value="income">收入</option>
                <option value="expense">支出</option>
              </select>
            </div>

            <div className="form-group">
              <label htmlFor="amount">金额</label>
              <input
                type="number"
                id="amount"
                name="amount"
                step="0.01"
                min="0"
                required
              />
            </div>

            <div className="form-group">
              <label htmlFor="category">分类</label>
              <select id="category" name="category" required>
                <option value="">选择分类</option>
                <option value="工资">工资</option>
                <option value="奖金">奖金</option>
                <option value="餐饮">餐饮</option>
                <option value="交通">交通</option>
                <option value="购物">购物</option>
                <option value="娱乐">娱乐</option>
              </select>
            </div>

            <div className="form-group">
              <label htmlFor="date">日期</label>
              <input
                type="date"
                id="date"
                name="date"
                defaultValue={new Date().toISOString().split('T')[0]}
                required
              />
            </div>
          </div>

          <div className="form-group">
            <label htmlFor="description">描述</label>
            <textarea
              id="description"
              name="description"
              rows={3}
              placeholder="输入交易描述..."
            />
          </div>

          <div className="form-actions">
            <button type="submit" className="btn btn-primary">
              保存交易
            </button>
            <a href="/transactions" className="btn btn-secondary">
              取消
            </a>
          </div>
        </Form>
      </div>
    </div>
  )
}

高级特性:数据加载与优化

1. 并行数据加载

// 使用Promise.all进行并行数据加载
export async function dashboardLoader() {
  const [transactions, categories] = await Promise.all([
    getTransactions(),
    getCategories()
  ])
  
  return {
    transactions,
    categories,
    stats: calculateStats(transactions)
  }
}

2. 延迟加载与代码分割

// 使用React.lazy进行路由懒加载
const Reports = lazy(() => import('./routes/Reports'))

// 路由配置中使用Suspense
<Route 
  path="reports" 
  element={
    <Suspense fallback={<div>加载中...</div>}>
      <Reports />
    </Suspense>
  } 
/>

3. 路由保护与权限控制

// 高阶组件实现路由保护
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const isAuthenticated = useAuth()
  const location = useLocation()
  
  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />
  }
  
  return <>{children}</>
}

// 使用方式
<Route
  path="/transactions"
  element={
    <ProtectedRoute>
      <Transactions />
    </ProtectedRoute>
  }
/>

样式设计与用户体验优化

CSS样式方案

/* 基础样式变量 */
:root {
  --primary-color: #2563eb;
  --secondary-color: #64748b;
  --success-color: #10b981;
  --danger-color: #ef4444;
  --warning-color: #f59e0b;
  --background-color: #f8fafc;
  --card-background: #ffffff;
  --border-color: #e2e8f0;
  --text-primary: #1e293b;
  --text-secondary: #64748b;
}

/* 响应式布局 */
.app-container {
  display: grid;
  grid-template-columns: 250px 1fr;
  min-height: 100vh;
}

/* 导航样式 */
.sidebar {
  background: var(--card-background);
  border-right: 1px solid var(--border-color);
  padding: 1rem;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  color: var(--text-secondary);
  text-decoration: none;
  transition: all 0.2s;
}

.nav-item:hover {
  background: var(--background-color);
  color: var(--primary-color);
}

.nav-item.active {
  background: var(--primary-color);
  color: white;
}

加载状态优化

// 使用useNavigation获取导航状态
function LoadingIndicator() {
  const navigation = useNavigation()
  
  return (
    <div className={`loading-overlay ${navigation.state === 'loading' ? 'visible' : ''}`}>
      <div className="loading-spinner">
        <Loader2 className="animate-spin" size={24} />
      </div>
    </div>
  )
}

错误处理与边界情况

错误边界组件

// src/routes/ErrorPage.tsx
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
import { AlertCircle, Home } from 'lucide-react'

export default function ErrorPage() {
  const error = useRouteError()
  
  let errorMessage = '发生了一个意外的错误'
  let errorStatus = 500
  
  if (isRouteErrorResponse(error)) {
    errorMessage = error.statusText
    errorStatus = error.status
  } else if (error instanceof Error) {
    errorMessage = error.message
  }

  return (
    <div className="error-container">
      <div className="error-content">
        <AlertCircle size={48} className="error-icon" />
        <h1>{errorStatus} 错误</h1>
        <p>{errorMessage}</p>
        <a href="/" className="btn btn-primary">
          <Home size={16} />
          返回首页
        </a>
      </div>
    </div>
  )
}

404页面处理

// 在路由配置中添加404路由
{
  path: '*',
  element: <NotFound />
}

// NotFound组件
function NotFound() {
  return (
    <div className="not-found">
      <h1>404 - 页面未找到</h1>
      <p>您访问的页面不存在</p>
      <Link to="/" className="btn btn-primary">
        返回首页
      </Link>
    </div>
  )
}

性能优化建议

1. 路由懒加载

import { lazy } from 'react'

const Transactions = lazy(() => import('./routes/Transactions'))
const Reports = lazy(() => import('./routes/Reports'))

// 使用Suspense包装
<Suspense fallback={<LoadingSpinner />}>
  <Transactions />
</Suspense>

2. 数据缓存策略

// 使用React Query或SWR进行数据缓存
import { useQuery } from '@tanstack/react-query'

function useTransactions() {
  return useQuery({
    queryKey: ['transactions'],
    queryFn: getTransactions,
    staleTime: 5 * 60 * 1000, // 5分钟缓存
  })
}

3. 代码分割优化

// 动态导入大型组件
const HeavyChartComponent = lazy(() => 
  import('./components/HeavyChartComponent').then(module => ({
    default: module.HeavyChartComponent
  }))
)

部署与生产环境配置

构建配置

# 构建生产版本
npm run build

# 预览构建结果
npm run preview

服务器配置示例(Nginx)

server {
    listen 80;
    server_name your-domain.com;
    root /path/to/your/build;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

总结与扩展建议

通过本教程,你已经学会了:

  1. ✅ React Router的基本配置和使用
  2. ✅ 数据路由的加载器和动作处理
  3. ✅ 嵌套路由和布局设计
  4. ✅ 表单处理和重定向
  5. ✅ 错误处理和用户体验优化

下一步学习建议

主题学习内容资源推荐

【免费下载链接】react-router 【免费下载链接】react-router 项目地址: https://gitcode.com/gh_mirrors/reac/react-router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值