学习目标
- 掌握React Router基础概念
- 学会路由配置和导航
- 理解动态路由和嵌套路由
- 掌握路由守卫和权限控制
- 构建完整的单页应用
学习时间安排
总时长:7-8小时
- React Router基础:2小时
- 路由配置和导航:2小时
- 动态路由和嵌套路由:1.5小时
- 路由守卫和权限控制:1小时
- 实践项目:2-3小时
第一部分:React Router基础 (2小时)
1.1 安装和配置
安装React Router
bash
# 安装React Router DOM
npm install react-router-dom
# 安装类型定义(如果使用TypeScript)
npm install --save-dev @types/react-router-dom
基础路由配置(详细注释版)
javascript
// src/App.js
// 导入React
import React from 'react';
// 导入路由组件
import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom';
// 导入页面组件
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
// 导入样式
import './App.css';
// 定义主应用组件
function App() {
return (
// 使用BrowserRouter包装整个应用
// BrowserRouter使用HTML5 History API
<BrowserRouter>
<div className="App">
{/* 导航栏 */}
<nav className="navbar">
<div className="nav-brand">
<h1>My React App</h1>
</div>
<ul className="nav-links">
{/* 使用Link组件创建导航链接 */}
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</nav>
{/* 主要内容区域 */}
<main className="main-content">
{/* 使用Routes组件定义路由规则 */}
<Routes>
{/* 定义根路径路由 */}
<Route path="/" element={<Home />} />
{/* 定义关于页面路由 */}
<Route path="/about" element={<About />} />
{/* 定义联系页面路由 */}
<Route path="/contact" element={<Contact />} />
{/* 重定向路由 */}
<Route path="/home" element={<Navigate to="/" replace />} />
{/* 404页面路由 - 使用*匹配所有未匹配的路径 */}
<Route path="*" element={<NotFound />} />
</Routes>
</main>
{/* 页脚 */}
<footer className="footer">
<p>© 2024 My React App. All rights reserved.</p>
</footer>
</div>
</BrowserRouter>
);
}
// 导出App组件
export default App;
1.2 基础页面组件
首页组件(详细注释版)
javascript
// src/pages/Home.js
// 导入React
import React from 'react';
// 导入Link组件用于导航
import { Link } from 'react-router-dom';
// 定义Home组件
function Home() {
return (
<div className="home-page">
<div className="hero-section">
<h1>Welcome to My React App</h1>
<p>This is the home page of our React application.</p>
{/* 使用Link组件创建导航链接 */}
<div className="cta-buttons">
<Link to="/about" className="btn btn-primary">
Learn More
</Link>
<Link to="/contact" className="btn btn-secondary">
Contact Us
</Link>
</div>
</div>
<div className="features-section">
<h2>Features</h2>
<div className="features-grid">
<div className="feature-card">
<h3>React Router</h3>
<p>Powerful routing for single-page applications</p>
</div>
<div className="feature-card">
<h3>Modern UI</h3>
<p>Beautiful and responsive user interface</p>
</div>
<div className="feature-card">
<h3>Fast Performance</h3>
<p>Optimized for speed and efficiency</p>
</div>
</div>
</div>
</div>
);
}
// 导出Home组件
export default Home;
关于页面组件(详细注释版)
javascript
// src/pages/About.js
// 导入React
import React from 'react';
// 导入Link组件
import { Link } from 'react-router-dom';
// 定义About组件
function About() {
return (
<div className="about-page">
<div className="page-header">
<h1>About Us</h1>
<p>Learn more about our company and mission</p>
</div>
<div className="about-content">
<section className="company-info">
<h2>Our Company</h2>
<p>
We are a leading technology company focused on creating
innovative solutions for modern web applications. Our team
consists of experienced developers and designers who are
passionate about delivering high-quality products.
</p>
</section>
<section className="team-info">
<h2>Our Team</h2>
<div className="team-grid">
<div className="team-member">
<h3>John Doe</h3>
<p>CEO & Founder</p>
</div>
<div className="team-member">
<h3>Jane Smith</h3>
<p>CTO</p>
</div>
<div className="team-member">
<h3>Bob Johnson</h3>
<p>Lead Developer</p>
</div>
</div>
</section>
<section className="mission-info">
<h2>Our Mission</h2>
<p>
To provide cutting-edge web solutions that help businesses
grow and succeed in the digital age. We believe in the power
of technology to transform ideas into reality.
</p>
</section>
</div>
<div className="page-actions">
<Link to="/contact" className="btn btn-primary">
Get in Touch
</Link>
<Link to="/" className="btn btn-secondary">
Back to Home
</Link>
</div>
</div>
);
}
// 导出About组件
export default About;
联系页面组件(详细注释版)
javascript
// src/pages/Contact.js
// 导入React和useState
import React, { useState } from 'react';
// 导入Link组件
import { Link } from 'react-router-dom';
// 定义Contact组件
function Contact() {
// 使用useState Hook管理表单状态
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState(null);
// 处理输入变化
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
setSubmitStatus('success');
setFormData({ name: '', email: '', subject: '', message: '' });
} catch (error) {
setSubmitStatus('error');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="contact-page">
<div className="page-header">
<h1>Contact Us</h1>
<p>Get in touch with us</p>
</div>
<div className="contact-content">
<div className="contact-info">
<h2>Get in Touch</h2>
<div className="contact-details">
<div className="contact-item">
<h3>Email</h3>
<p>contact@myreactapp.com</p>
</div>
<div className="contact-item">
<h3>Phone</h3>
<p>+1 (555) 123-4567</p>
</div>
<div className="contact-item">
<h3>Address</h3>
<p>123 Main Street<br />City, State 12345</p>
</div>
</div>
</div>
<div className="contact-form">
<h2>Send us a Message</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="5"
required
/>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
{submitStatus === 'success' && (
<div className="success-message">
Message sent successfully!
</div>
)}
{submitStatus === 'error' && (
<div className="error-message">
Failed to send message. Please try again.
</div>
)}
</div>
</div>
<div className="page-actions">
<Link to="/" className="btn btn-secondary">
Back to Home
</Link>
</div>
</div>
);
}
// 导出Contact组件
export default Contact;
404页面组件(详细注释版)
javascript
// src/pages/NotFound.js
// 导入React
import React from 'react';
// 导入Link和useNavigate
import { Link, useNavigate } from 'react-router-dom';
// 定义NotFound组件
function NotFound() {
// 使用useNavigate Hook获取导航函数
const navigate = useNavigate();
// 处理返回上一页
const handleGoBack = () => {
navigate(-1);
};
// 处理返回首页
const handleGoHome = () => {
navigate('/');
};
return (
<div className="not-found-page">
<div className="error-content">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>
Sorry, the page you are looking for does not exist.
</p>
<div className="error-actions">
<button onClick={handleGoBack} className="btn btn-secondary">
Go Back
</button>
<button onClick={handleGoHome} className="btn btn-primary">
Go Home
</button>
<Link to="/contact" className="btn btn-outline">
Contact Support
</Link>
</div>
</div>
</div>
);
}
// 导出NotFound组件
export default NotFound;
第二部分:路由配置和导航 (2小时)
2.1 路由参数和查询参数
动态路由组件(详细注释版)
javascript
// src/pages/UserProfile.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useSearchParams, useLocation } from 'react-router-dom';
// 定义UserProfile组件
function UserProfile() {
// 使用useParams Hook获取路由参数
const { userId } = useParams();
// 使用useSearchParams Hook获取查询参数
const [searchParams, setSearchParams] = useSearchParams();
// 使用useLocation Hook获取位置信息
const location = useLocation();
// 使用useState Hook管理状态
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 从查询参数中获取值
const tab = searchParams.get('tab') || 'profile';
const edit = searchParams.get('edit') === 'true';
// 获取用户数据
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
// 模拟API调用
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
// 处理标签页切换
const handleTabChange = (newTab) => {
setSearchParams({ tab: newTab });
};
// 处理编辑模式切换
const handleEditToggle = () => {
setSearchParams(prev => ({
...Object.fromEntries(prev),
edit: edit ? 'false' : 'true'
}));
};
// 处理返回
const handleGoBack = () => {
navigate(-1);
};
if (loading) {
return <div className="loading">Loading user profile...</div>;
}
if (error) {
return (
<div className="error">
<h2>Error</h2>
<p>{error}</p>
<button onClick={handleGoBack}>Go Back</button>
</div>
);
}
if (!user) {
return <div className="not-found">User not found</div>;
}
return (
<div className="user-profile">
<div className="profile-header">
<button onClick={handleGoBack} className="back-btn">
← Back
</button>
<h1>User Profile: {user.name}</h1>
<button onClick={handleEditToggle} className="edit-btn">
{edit ? 'Cancel Edit' : 'Edit Profile'}
</button>
</div>
<div className="profile-tabs">
<button
className={tab === 'profile' ? 'active' : ''}
onClick={() => handleTabChange('profile')}
>
Profile
</button>
<button
className={tab === 'settings' ? 'active' : ''}
onClick={() => handleTabChange('settings')}
>
Settings
</button>
<button
className={tab === 'activity' ? 'active' : ''}
onClick={() => handleTabChange('activity')}
>
Activity
</button>
</div>
<div className="profile-content">
{tab === 'profile' && (
<div className="profile-info">
<h2>Profile Information</h2>
<div className="info-grid">
<div className="info-item">
<label>Name:</label>
<span>{user.name}</span>
</div>
<div className="info-item">
<label>Email:</label>
<span>{user.email}</span>
</div>
<div className="info-item">
<label>Phone:</label>
<span>{user.phone}</span>
</div>
<div className="info-item">
<label>Location:</label>
<span>{user.location}</span>
</div>
</div>
</div>
)}
{tab === 'settings' && (
<div className="settings-info">
<h2>Account Settings</h2>
<p>Settings content goes here...</p>
</div>
)}
{tab === 'activity' && (
<div className="activity-info">
<h2>Recent Activity</h2>
<p>Activity content goes here...</p>
</div>
)}
</div>
<div className="profile-debug">
<h3>Debug Information</h3>
<p>User ID: {userId}</p>
<p>Current Tab: {tab}</p>
<p>Edit Mode: {edit ? 'Yes' : 'No'}</p>
<p>Location: {location.pathname}</p>
<p>Search: {location.search}</p>
</div>
</div>
);
}
// 导出UserProfile组件
export default UserProfile;
2.2 嵌套路由
嵌套路由配置(详细注释版)
javascript
// src/App.js
// 导入React
import React from 'react';
// 导入路由组件
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
// 导入布局组件
import Layout from './components/Layout';
// 导入页面组件
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import UserProfile from './pages/UserProfile';
import UserSettings from './pages/UserSettings';
import UserActivity from './pages/UserActivity';
import NotFound from './pages/NotFound';
// 定义主应用组件
function App() {
return (
<BrowserRouter>
<Routes>
{/* 根路径路由 */}
<Route path="/" element={<Layout />}>
{/* 嵌套路由 */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
{/* 用户相关嵌套路由 */}
<Route path="user/:userId" element={<UserProfile />}>
<Route path="settings" element={<UserSettings />} />
<Route path="activity" element={<UserActivity />} />
</Route>
</Route>
{/* 404页面路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// 导出App组件
export default App;
布局组件(详细注释版)
javascript
// src/components/Layout.js
// 导入React
import React from 'react';
// 导入路由组件
import { Link, Outlet, useLocation } from 'react-router-dom';
// 定义Layout组件
function Layout() {
// 使用useLocation Hook获取当前路径
const location = useLocation();
// 检查是否为活动路径
const isActive = (path) => {
return location.pathname === path;
};
return (
<div className="layout">
{/* 头部导航 */}
<header className="header">
<div className="header-content">
<div className="logo">
<Link to="/">My React App</Link>
</div>
<nav className="nav">
<ul className="nav-list">
<li>
<Link
to="/"
className={isActive('/') ? 'active' : ''}
>
Home
</Link>
</li>
<li>
<Link
to="/about"
className={isActive('/about') ? 'active' : ''}
>
About
</Link>
</li>
<li>
<Link
to="/contact"
className={isActive('/contact') ? 'active' : ''}
>
Contact
</Link>
</li>
</ul>
</nav>
</div>
</header>
{/* 主要内容区域 */}
<main className="main">
{/* 使用Outlet组件渲染嵌套路由 */}
<Outlet />
</main>
{/* 页脚 */}
<footer className="footer">
<div className="footer-content">
<p>© 2024 My React App. All rights reserved.</p>
<div className="footer-links">
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</div>
</div>
</footer>
</div>
);
}
// 导出Layout组件
export default Layout;
2.3 编程式导航
导航Hook组件(详细注释版)
javascript
// src/components/Navigation.js
// 导入React和useState
import React, { useState } from 'react';
// 导入路由Hooks
import { useNavigate, useLocation, useParams } from 'react-router-dom';
// 定义Navigation组件
function Navigation() {
// 使用路由Hooks
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
// 使用useState Hook管理状态
const [searchQuery, setSearchQuery] = useState('');
// 处理导航到特定路径
const handleNavigate = (path) => {
navigate(path);
};
// 处理返回上一页
const handleGoBack = () => {
navigate(-1);
};
// 处理前进到下一页
const handleGoForward = () => {
navigate(1);
};
// 处理搜索
const handleSearch = (e) => {
e.preventDefault();
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
}
};
// 处理用户导航
const handleUserNavigate = (userId) => {
navigate(`/user/${userId}`);
};
// 处理带状态的导航
const handleNavigateWithState = (path, state) => {
navigate(path, { state });
};
// 处理替换当前历史记录
const handleReplace = (path) => {
navigate(path, { replace: true });
};
// 处理相对导航
const handleRelativeNavigate = (path) => {
navigate(path, { relative: 'path' });
};
return (
<div className="navigation">
<div className="nav-controls">
<button onClick={handleGoBack} className="nav-btn">
← Back
</button>
<button onClick={handleGoForward} className="nav-btn">
Forward →
</button>
</div>
<div className="nav-links">
<button
onClick={() => handleNavigate('/')}
className="nav-link"
>
Home
</button>
<button
onClick={() => handleNavigate('/about')}
className="nav-link"
>
About
</button>
<button
onClick={() => handleNavigate('/contact')}
className="nav-link"
>
Contact
</button>
</div>
<div className="nav-actions">
<button
onClick={() => handleUserNavigate(123)}
className="nav-link"
>
User 123
</button>
<button
onClick={() => handleUserNavigate(456)}
className="nav-link"
>
User 456
</button>
</div>
<form onSubmit={handleSearch} className="search-form">
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
<button type="submit" className="search-btn">
Search
</button>
</form>
<div className="nav-info">
<p>Current Path: {location.pathname}</p>
<p>Search: {location.search}</p>
<p>Params: {JSON.stringify(params)}</p>
<p>State: {JSON.stringify(location.state)}</p>
</div>
</div>
);
}
// 导出Navigation组件
export default Navigation;
第三部分:动态路由和嵌套路由 (1.5小时)
3.1 动态路由参数
产品详情页面(详细注释版)
javascript
// src/pages/ProductDetail.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useNavigate, useLocation } from 'react-router-dom';
// 定义ProductDetail组件
function ProductDetail() {
// 使用路由Hooks
const { productId, categoryId } = useParams();
const navigate = useNavigate();
const location = useLocation();
// 使用useState Hook管理状态
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [quantity, setQuantity] = useState(1);
// 获取产品数据
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
// 模拟API调用
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('Product not found');
}
const productData = await response.json();
setProduct(productData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (productId) {
fetchProduct();
}
}, [productId]);
// 处理添加到购物车
const handleAddToCart = () => {
// 模拟添加到购物车
console.log(`Added ${quantity} of product ${productId} to cart`);
navigate('/cart', {
state: {
product: product,
quantity: quantity,
from: location.pathname
}
});
};
// 处理购买
const handleBuyNow = () => {
navigate('/checkout', {
state: {
products: [{ product, quantity }],
from: location.pathname
}
});
};
// 处理返回
const handleGoBack = () => {
if (location.state?.from) {
navigate(location.state.from);
} else if (categoryId) {
navigate(`/category/${categoryId}`);
} else {
navigate('/products');
}
};
if (loading) {
return <div className="loading">Loading product...</div>;
}
if (error) {
return (
<div className="error">
<h2>Error</h2>
<p>{error}</p>
<button onClick={handleGoBack}>Go Back</button>
</div>
);
}
if (!product) {
return <div className="not-found">Product not found</div>;
}
return (
<div className="product-detail">
<div className="product-header">
<button onClick={handleGoBack} className="back-btn">
← Back
</button>
<h1>{product.name}</h1>
</div>
<div className="product-content">
<div className="product-image">
<img src={product.image} alt={product.name} />
</div>
<div className="product-info">
<h2>{product.name}</h2>
<p className="product-price">${product.price}</p>
<p className="product-description">{product.description}</p>
<div className="product-details">
<h3>Product Details</h3>
<ul>
<li>Category: {product.category}</li>
<li>Brand: {product.brand}</li>
<li>SKU: {product.sku}</li>
<li>Stock: {product.stock}</li>
</ul>
</div>
<div className="product-actions">
<div className="quantity-selector">
<label htmlFor="quantity">Quantity:</label>
<input
type="number"
id="quantity"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value) || 1)}
min="1"
max={product.stock}
/>
</div>
<div className="action-buttons">
<button
onClick={handleAddToCart}
className="btn btn-primary"
>
Add to Cart
</button>
<button
onClick={handleBuyNow}
className="btn btn-secondary"
>
Buy Now
</button>
</div>
</div>
</div>
</div>
<div className="product-debug">
<h3>Debug Information</h3>
<p>Product ID: {productId}</p>
<p>Category ID: {categoryId}</p>
<p>Current Path: {location.pathname}</p>
<p>From: {location.state?.from || 'Unknown'}</p>
</div>
</div>
);
}
// 导出ProductDetail组件
export default ProductDetail;
3.2 嵌套路由实现
用户设置页面(详细注释版)
javascript
// src/pages/UserSettings.js
// 导入React和useState
import React, { useState } from 'react';
// 导入路由Hooks
import { useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';
// 定义UserSettings组件
function UserSettings() {
// 使用路由Hooks
const { userId } = useParams();
const navigate = useNavigate();
const location = useLocation();
// 使用useState Hook管理状态
const [activeTab, setActiveTab] = useState('profile');
// 处理标签页切换
const handleTabChange = (tab) => {
setActiveTab(tab);
navigate(`/user/${userId}/settings/${tab}`);
};
// 处理返回
const handleGoBack = () => {
navigate(`/user/${userId}`);
};
return (
<div className="user-settings">
<div className="settings-header">
<button onClick={handleGoBack} className="back-btn">
← Back to Profile
</button>
<h1>User Settings</h1>
</div>
<div className="settings-tabs">
<button
className={activeTab === 'profile' ? 'active' : ''}
onClick={() => handleTabChange('profile')}
>
Profile
</button>
<button
className={activeTab === 'account' ? 'active' : ''}
onClick={() => handleTabChange('account')}
>
Account
</button>
<button
className={activeTab === 'security' ? 'active' : ''}
onClick={() => handleTabChange('security')}
>
Security
</button>
<button
className={activeTab === 'notifications' ? 'active' : ''}
onClick={() => handleTabChange('notifications')}
>
Notifications
</button>
</div>
<div className="settings-content">
{/* 使用Outlet组件渲染嵌套路由 */}
<Outlet />
</div>
</div>
);
}
// 导出UserSettings组件
export default UserSettings;
用户活动页面(详细注释版)
javascript
// src/pages/UserActivity.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useNavigate } from 'react-router-dom';
// 定义UserActivity组件
function UserActivity() {
// 使用路由Hooks
const { userId } = useParams();
const navigate = useNavigate();
// 使用useState Hook管理状态
const [activities, setActivities] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
// 获取用户活动数据
useEffect(() => {
const fetchActivities = async () => {
try {
setLoading(true);
// 模拟API调用
const response = await fetch(`/api/users/${userId}/activities`);
if (!response.ok) {
throw new Error('Failed to fetch activities');
}
const activitiesData = await response.json();
setActivities(activitiesData);
} catch (error) {
console.error('Error fetching activities:', error);
} finally {
setLoading(false);
}
};
if (userId) {
fetchActivities();
}
}, [userId]);
// 处理返回
const handleGoBack = () => {
navigate(`/user/${userId}`);
};
// 过滤活动
const filteredActivities = activities.filter(activity => {
if (filter === 'all') return true;
return activity.type === filter;
});
if (loading) {
return <div className="loading">Loading activities...</div>;
}
return (
<div className="user-activity">
<div className="activity-header">
<button onClick={handleGoBack} className="back-btn">
← Back to Profile
</button>
<h1>User Activity</h1>
</div>
<div className="activity-filters">
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
<option value="all">All Activities</option>
<option value="login">Logins</option>
<option value="action">Actions</option>
<option value="system">System</option>
</select>
</div>
<div className="activity-list">
{filteredActivities.length === 0 ? (
<p>No activities found</p>
) : (
filteredActivities.map(activity => (
<div key={activity.id} className="activity-item">
<div className="activity-icon">
{activity.type === 'login' ? '🔐' :
activity.type === 'action' ? '⚡' : '🔧'}
</div>
<div className="activity-content">
<h3>{activity.title}</h3>
<p>{activity.description}</p>
<span className="activity-time">
{new Date(activity.timestamp).toLocaleString()}
</span>
</div>
</div>
))
)}
</div>
</div>
);
}
// 导出UserActivity组件
export default UserActivity;
第四部分:路由守卫和权限控制 (1小时)
4.1 路由守卫实现
认证守卫组件(详细注释版)
javascript
// src/components/ProtectedRoute.js
// 导入React
import React from 'react';
// 导入路由组件
import { Navigate, useLocation } from 'react-router-dom';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义ProtectedRoute组件
function ProtectedRoute({ children, requiredRole = null, requiredPermission = null }) {
// 使用认证Hook
const { user, isAuthenticated, hasRole, hasPermission } = useAuth();
const location = useLocation();
// 检查是否已认证
if (!isAuthenticated) {
// 重定向到登录页面,并保存当前路径
return (
<Navigate
to="/login"
state={{ from: location.pathname }}
replace
/>
);
}
// 检查角色权限
if (requiredRole && !hasRole(requiredRole)) {
return (
<Navigate
to="/unauthorized"
state={{
message: `You need ${requiredRole} role to access this page`,
from: location.pathname
}}
replace
/>
);
}
// 检查具体权限
if (requiredPermission && !hasPermission(requiredPermission)) {
return (
<Navigate
to="/unauthorized"
state={{
message: `You need ${requiredPermission} permission to access this page`,
from: location.pathname
}}
replace
/>
);
}
// 如果所有检查都通过,渲染子组件
return children;
}
// 导出ProtectedRoute组件
export default ProtectedRoute;
权限控制组件(详细注释版)
javascript
// src/components/PermissionGate.js
// 导入React
import React from 'react';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义PermissionGate组件
function PermissionGate({
children,
role = null,
permission = null,
fallback = null,
requireAll = false
}) {
// 使用认证Hook
const { hasRole, hasPermission } = useAuth();
// 检查角色权限
const hasRoleAccess = role ? hasRole(role) : true;
// 检查具体权限
const hasPermissionAccess = permission ? hasPermission(permission) : true;
// 根据requireAll参数决定权限检查逻辑
const hasAccess = requireAll
? hasRoleAccess && hasPermissionAccess
: hasRoleAccess || hasPermissionAccess;
// 如果没有权限,返回fallback或null
if (!hasAccess) {
return fallback;
}
// 如果有权限,渲染子组件
return children;
}
// 导出PermissionGate组件
export default PermissionGate;
4.2 路由配置和权限
带权限的路由配置(详细注释版)
javascript
// src/App.js
// 导入React
import React from 'react';
// 导入路由组件
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 导入布局组件
import Layout from './components/Layout';
// 导入守卫组件
import ProtectedRoute from './components/ProtectedRoute';
import PermissionGate from './components/PermissionGate';
// 导入页面组件
import Home from './pages/Home';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import AdminPanel from './pages/AdminPanel';
import UserProfile from './pages/UserProfile';
import Unauthorized from './pages/Unauthorized';
import NotFound from './pages/NotFound';
// 定义主应用组件
function App() {
return (
<BrowserRouter>
<Routes>
{/* 公共路由 */}
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route path="unauthorized" element={<Unauthorized />} />
</Route>
{/* 受保护的路由 */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
{/* 需要特定角色的路由 */}
<Route path="/admin" element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
} />
{/* 需要特定权限的路由 */}
<Route path="/user/:userId" element={
<ProtectedRoute requiredPermission="read:users">
<UserProfile />
</ProtectedRoute>
} />
{/* 404页面路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// 导出App组件
export default App;
登录页面(详细注释版)
javascript
// src/pages/Login.js
// 导入React和useState
import React, { useState } from 'react';
// 导入路由Hooks
import { useNavigate, useLocation } from 'react-router-dom';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义Login组件
function Login() {
// 使用路由Hooks
const navigate = useNavigate();
const location = useLocation();
// 使用认证Hook
const { login, isLoading, error } = useAuth();
// 使用useState Hook管理表单状态
const [formData, setFormData] = useState({
email: '',
password: ''
});
// 获取重定向路径
const from = location.state?.from || '/dashboard';
// 处理输入变化
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(formData);
// 登录成功后重定向到原始路径
navigate(from, { replace: true });
} catch (error) {
console.error('Login failed:', error);
}
};
return (
<div className="login-page">
<div className="login-container">
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={isLoading}
>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
{error && (
<div className="error-message">
{error}
</div>
)}
<div className="login-info">
<p>Redirecting to: {from}</p>
</div>
</div>
</div>
);
}
// 导出Login组件
export default Login;
未授权页面(详细注释版)
javascript
// src/pages/Unauthorized.js
// 导入React
import React from 'react';
// 导入路由Hooks
import { useLocation, useNavigate } from 'react-router-dom';
// 定义Unauthorized组件
function Unauthorized() {
// 使用路由Hooks
const location = useLocation();
const navigate = useNavigate();
// 获取错误信息
const message = location.state?.message || 'You are not authorized to access this page.';
const from = location.state?.from || '/';
// 处理返回
const handleGoBack = () => {
navigate(from);
};
// 处理返回首页
const handleGoHome = () => {
navigate('/');
};
return (
<div className="unauthorized-page">
<div className="error-content">
<h1>403</h1>
<h2>Unauthorized Access</h2>
<p>{message}</p>
<div className="error-actions">
<button onClick={handleGoBack} className="btn btn-secondary">
Go Back
</button>
<button onClick={handleGoHome} className="btn btn-primary">
Go Home
</button>
</div>
</div>
</div>
);
}
// 导出Unauthorized组件
export default Unauthorized;
第五部分:实践项目(详细注释版)
项目:完整的博客系统
主应用组件(详细注释版)
javascript
// src/App.js
// 导入React
import React from 'react';
// 导入路由组件
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 导入布局组件
import Layout from './components/Layout';
// 导入守卫组件
import ProtectedRoute from './components/ProtectedRoute';
// 导入页面组件
import Home from './pages/Home';
import Blog from './pages/Blog';
import PostDetail from './pages/PostDetail';
import CreatePost from './pages/CreatePost';
import EditPost from './pages/EditPost';
import UserProfile from './pages/UserProfile';
import Login from './pages/Login';
import Register from './pages/Register';
import NotFound from './pages/NotFound';
// 定义主应用组件
function App() {
return (
<BrowserRouter>
<Routes>
{/* 公共路由 */}
<Route path="/" element={<Layout />}>
<Route index element={<Home />}
<Route path="blog" element={<Blog />} />
<Route path="blog/:postId" element={<PostDetail />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Route>
{/* 受保护的路由 */}
<Route path="/create-post" element={
<ProtectedRoute requiredPermission="write:posts">
<CreatePost />
</ProtectedRoute>
} />
<Route path="/edit-post/:postId" element={
<ProtectedRoute requiredPermission="write:posts">
<EditPost />
</ProtectedRoute>
} />
<Route path="/user/:userId" element={
<ProtectedRoute>
<UserProfile />
</ProtectedRoute>
} />
{/* 404页面路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// 导出App组件
export default App;
博客列表页面(详细注释版)
javascript
// src/pages/Blog.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useNavigate, useSearchParams } from 'react-router-dom';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义Blog组件
function Blog() {
// 使用路由Hooks
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
// 使用认证Hook
const { hasPermission } = useAuth();
// 使用useState Hook管理状态
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [category, setCategory] = useState('all');
// 从URL参数中获取搜索查询
useEffect(() => {
const query = searchParams.get('q') || '';
const cat = searchParams.get('category') || 'all';
setSearchQuery(query);
setCategory(cat);
}, [searchParams]);
// 获取博客文章
useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true);
// 模拟API调用
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
const postsData = await response.json();
setPosts(postsData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
// 处理搜索
const handleSearch = (e) => {
e.preventDefault();
const params = new URLSearchParams();
if (searchQuery.trim()) {
params.set('q', searchQuery.trim());
}
if (category !== 'all') {
params.set('category', category);
}
setSearchParams(params);
};
// 处理分类过滤
const handleCategoryChange = (newCategory) => {
setCategory(newCategory);
const params = new URLSearchParams();
if (searchQuery.trim()) {
params.set('q', searchQuery.trim());
}
if (newCategory !== 'all') {
params.set('category', newCategory);
}
setSearchParams(params);
};
// 处理文章点击
const handlePostClick = (postId) => {
navigate(`/blog/${postId}`);
};
// 处理创建文章
const handleCreatePost = () => {
navigate('/create-post');
};
// 过滤文章
const filteredPosts = posts.filter(post => {
const matchesSearch = !searchQuery.trim() ||
post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.content.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = category === 'all' || post.category === category;
return matchesSearch && matchesCategory;
});
if (loading) {
return <div className="loading">Loading posts...</div>;
}
if (error) {
return <div className="error">Error: {error}</div>;
}
return (
<div className="blog-page">
<div className="blog-header">
<h1>Blog</h1>
{hasPermission('write:posts') && (
<button onClick={handleCreatePost} className="btn btn-primary">
Create Post
</button>
)}
</div>
<div className="blog-filters">
<form onSubmit={handleSearch} className="search-form">
<input
type="text"
placeholder="Search posts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
<button type="submit" className="search-btn">Search</button>
</form>
<div className="category-filters">
<button
className={category === 'all' ? 'active' : ''}
onClick={() => handleCategoryChange('all')}
>
All
</button>
<button
className={category === 'technology' ? 'active' : ''}
onClick={() => handleCategoryChange('technology')}
>
Technology
</button>
<button
className={category === 'lifestyle' ? 'active' : ''}
onClick={() => handleCategoryChange('lifestyle')}
>
Lifestyle
</button>
<button
className={category === 'business' ? 'active' : ''}
onClick={() => handleCategoryChange('business')}
>
Business
</button>
</div>
</div>
<div className="blog-content">
{filteredPosts.length === 0 ? (
<div className="no-posts">
<h2>No posts found</h2>
<p>Try adjusting your search or filter criteria.</p>
</div>
) : (
<div className="posts-grid">
{filteredPosts.map(post => (
<div
key={post.id}
className="post-card"
onClick={() => handlePostClick(post.id)}
>
<div className="post-image">
<img src={post.image} alt={post.title} />
</div>
<div className="post-content">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
<div className="post-meta">
<span className="post-author">{post.author}</span>
<span className="post-date">
{new Date(post.createdAt).toLocaleDateString()}
</span>
<span className="post-category">{post.category}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// 导出Blog组件
export default Blog;
文章详情页面(详细注释版)
javascript
// src/pages/PostDetail.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useNavigate, useLocation } from 'react-router-dom';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义PostDetail组件
function PostDetail() {
// 使用路由Hooks
const { postId } = useParams();
const navigate = useNavigate();
const location = useLocation();
// 使用认证Hook
const { user, hasPermission } = useAuth();
// 使用useState Hook管理状态
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState('');
// 获取文章详情
useEffect(() => {
const fetchPost = async () => {
try {
setLoading(true);
// 模拟API调用
const response = await fetch(`/api/posts/${postId}`);
if (!response.ok) {
throw new Error('Post not found');
}
const postData = await response.json();
setPost(postData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (postId) {
fetchPost();
}
}, [postId]);
// 获取评论
useEffect(() => {
const fetchComments = async () => {
try {
const response = await fetch(`/api/posts/${postId}/comments`);
if (response.ok) {
const commentsData = await response.json();
setComments(commentsData);
}
} catch (err) {
console.error('Error fetching comments:', err);
}
};
if (postId) {
fetchComments();
}
}, [postId]);
// 处理添加评论
const handleAddComment = async (e) => {
e.preventDefault();
if (!newComment.trim()) return;
try {
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: newComment,
author: user?.name || 'Anonymous'
})
});
if (response.ok) {
const comment = await response.json();
setComments(prev => [...prev, comment]);
setNewComment('');
}
} catch (err) {
console.error('Error adding comment:', err);
}
};
// 处理编辑文章
const handleEditPost = () => {
navigate(`/edit-post/${postId}`);
};
// 处理删除文章
const handleDeletePost = async () => {
if (!window.confirm('Are you sure you want to delete this post?')) {
return;
}
try {
const response = await fetch(`/api/posts/${postId}`, {
method: 'DELETE'
});
if (response.ok) {
navigate('/blog');
}
} catch (err) {
console.error('Error deleting post:', err);
}
};
// 处理返回
const handleGoBack = () => {
if (location.state?.from) {
navigate(location.state.from);
} else {
navigate('/blog');
}
};
if (loading) {
return <div className="loading">Loading post...</div>;
}
if (error) {
return (
<div className="error">
<h2>Error</h2>
<p>{error}</p>
<button onClick={handleGoBack}>Go Back</button>
</div>
);
}
if (!post) {
return <div className="not-found">Post not found</div>;
}
return (
<div className="post-detail">
<div className="post-header">
<button onClick={handleGoBack} className="back-btn">
← Back to Blog
</button>
<h1>{post.title}</h1>
{hasPermission('write:posts') && (
<div className="post-actions">
<button onClick={handleEditPost} className="btn btn-secondary">
Edit
</button>
<button onClick={handleDeletePost} className="btn btn-danger">
Delete
</button>
</div>
)}
</div>
<div className="post-content">
<div className="post-meta">
<span className="post-author">By {post.author}</span>
<span className="post-date">
{new Date(post.createdAt).toLocaleDateString()}
</span>
<span className="post-category">{post.category}</span>
</div>
<div className="post-image">
<img src={post.image} alt={post.title} />
</div>
<div className="post-body">
{post.content}
</div>
</div>
<div className="post-comments">
<h2>Comments ({comments.length})</h2>
<div className="comments-list">
{comments.map(comment => (
<div key={comment.id} className="comment">
<div className="comment-header">
<span className="comment-author">{comment.author}</span>
<span className="comment-date">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
<div className="comment-content">
{comment.content}
</div>
</div>
))}
</div>
<form onSubmit={handleAddComment} className="comment-form">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Write a comment..."
rows="3"
/>
<button type="submit" className="btn btn-primary">
Add Comment
</button>
</form>
</div>
</div>
);
}
// 导出PostDetail组件
export default PostDetail;
样式文件(详细注释版)
css
/* src/App.css */
/* 应用主容器样式 */
.App {
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
}
/* 布局样式 */
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background-color: #333;
color: white;
padding: 1rem;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.logo a {
color: white;
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.nav-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}
.nav-list a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-list a:hover,
.nav-list a.active {
background-color: #555;
}
.main {
flex: 1;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.footer {
background-color: #f8f9fa;
padding: 2rem;
text-align: center;
border-top: 1px solid #dee2e6;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-links {
display: flex;
gap: 1rem;
}
.footer-links a {
color: #6c757d;
text-decoration: none;
}
/* 页面样式 */
.home-page {
text-align: center;
}
.hero-section {
padding: 4rem 0;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 3rem;
}
.hero-section h1 {
font-size: 3rem;
margin-bottom: 1rem;
color: #333;
}
.hero-section p {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.features-section {
margin-top: 3rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.feature-card {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.feature-card h3 {
color: #333;
margin-bottom: 1rem;
}
.feature-card p {
color: #666;
line-height: 1.6;
}
/* 博客页面样式 */
.blog-page {
max-width: 1200px;
margin: 0 auto;
}
.blog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.blog-header h1 {
margin: 0;
color: #333;
}
.blog-filters {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
}
.search-form {
display: flex;
gap: 0.5rem;
}
.search-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.search-btn {
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.category-filters button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background-color: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.category-filters button:hover,
.category-filters button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.post-card {
background-color: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.post-image img {
width: 100%;
height: 200px;
object-fit: cover;
}
.post-content {
padding: 1.5rem;
}
.post-content h3 {
margin: 0 0 1rem 0;
color: #333;
font-size: 1.2rem;
}
.post-content p {
color: #666;
line-height: 1.6;
margin-bottom: 1rem;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: #999;
}
.post-meta span {
background-color: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
/* 文章详情页面样式 */
.post-detail {
max-width: 800px;
margin: 0 auto;
}
.post-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #dee2e6;
}
.post-header h1 {
margin: 0;
color: #333;
flex: 1;
}
.post-actions {
display: flex;
gap: 0.5rem;
}
.post-content {
margin-bottom: 3rem;
}
.post-meta {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
font-size: 0.9rem;
color: #666;
}
.post-image img {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 2rem;
}
.post-body {
font-size: 1.1rem;
line-height: 1.8;
color: #333;
}
.post-comments {
border-top: 1px solid #dee2e6;
padding-top: 2rem;
}
.post-comments h2 {
margin-bottom: 1.5rem;
color: #333;
}
.comments-list {
margin-bottom: 2rem;
}
.comment {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.comment-content {
color: #333;
line-height: 1.6;
}
.comment-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.comment-form textarea {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
resize: vertical;
}
/* 按钮样式 */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
text-decoration: none;
display: inline-block;
text-align: center;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-outline {
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
}
.btn-outline:hover {
background-color: #007bff;
color: white;
}
/* 工具类 */
.loading {
text-align: center;
padding: 2rem;
font-size: 1.2rem;
color: #666;
}
.error {
text-align: center;
padding: 2rem;
color: #dc3545;
}
.not-found {
text-align: center;
padding: 2rem;
color: #666;
}
.back-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-bottom: 1rem;
}
.back-btn:hover {
background-color: #545b62;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.nav-list {
flex-direction: column;
gap: 0.5rem;
}
.main {
padding: 1rem;
}
.blog-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.search-form {
flex-direction: column;
}
.category-filters {
justify-content: center;
}
.posts-grid {
grid-template-columns: 1fr;
}
.post-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.post-actions {
justify-content: center;
}
.footer-content {
flex-direction: column;
gap: 1rem;
}
}
练习题目
基础练习
- 路由基础练习
javascript
// 练习1:创建一个简单的导航系统
// 实现:首页、关于、联系、404页面
// 包含:导航栏、页脚、响应式设计
// 练习2:实现动态路由
// 实现:用户详情页、产品详情页
// 包含:路由参数、查询参数、编程式导航
- 嵌套路由练习
javascript
// 练习3:实现用户管理系统的嵌套路由
// 实现:用户列表、用户详情、用户设置
// 包含:嵌套布局、Outlet组件
// 练习4:实现博客系统的嵌套路由
// 实现:文章列表、文章详情、评论系统
// 包含:动态路由、状态管理
进阶练习
- 路由守卫练习
javascript
// 练习5:实现完整的权限控制系统
// 实现:登录、注册、权限验证
// 包含:路由守卫、权限控制、状态管理
// 练习6:实现企业级应用的路由系统
// 实现:多级路由、权限控制、状态管理
// 包含:最佳实践、性能优化
- 综合应用练习
javascript
// 练习7:构建完整的电商系统
// 实现:商品展示、购物车、订单管理
// 包含:路由配置、状态管理、权限控制
学习检查点
完成标准
- 理解React Router基础概念
- 掌握路由配置和导航
- 学会动态路由和嵌套路由
- 掌握路由守卫和权限控制
- 完成所有练习题目
自我测试
- React Router的核心概念是什么?
- 如何实现动态路由和嵌套路由?
- 路由守卫的作用是什么?
- 如何优化路由性能?