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. 路由配置方式
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";
}
}
总结与扩展建议
通过本教程,你已经学会了:
- ✅ React Router的基本配置和使用
- ✅ 数据路由的加载器和动作处理
- ✅ 嵌套路由和布局设计
- ✅ 表单处理和重定向
- ✅ 错误处理和用户体验优化
下一步学习建议
主题 | 学习内容 | 资源推荐 |
---|
【免费下载链接】react-router 项目地址: https://gitcode.com/gh_mirrors/reac/react-router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考