React 18.x 学习计划 - 第六天:React路由和导航
学习目标
- 掌握React Router基础概念
- 学会路由配置和导航
- 理解动态路由和嵌套路由
- 掌握路由守卫和权限控制
- 构建完整的单页应用
学习时间安排
总时长:7-8小时
- React Router基础:2小时
- 路由配置和导航:2小时
- 动态路由和嵌套路由:1.5小时
- 路由守卫和权限控制:1小时
- 实践项目:2-3小时
第一部分:React Router基础 (2小时)
1.1 安装和配置
安装React Router
# 安装React Router DOM
npm install react-router-dom# 安装类型定义(如果使用TypeScript)
npm install --save-dev @types/react-router-dom
基础路由配置(详细注释版)
// 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 基础页面组件
首页组件(详细注释版)
// 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;
关于页面组件(详细注释版)
// 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;
联系页面组件(详细注释版)
// 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><inputtype="text"id="name"name="name"value={formData.name}onChange={handleChange}required/></div><div className="form-group"><label htmlFor="email">Email</label><inputtype="email"id="email"name="email"value={formData.email}onChange={handleChange}required/></div><div className="form-group"><label htmlFor="subject">Subject</label><inputtype="text"id="subject"name="subject"value={formData.subject}onChange={handleChange}required/></div><div className="form-group"><label htmlFor="message">Message</label><textareaid="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页面组件(详细注释版)
// 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 路由参数和查询参数
动态路由组件(详细注释版)
// 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 嵌套路由
嵌套路由配置(详细注释版)
// 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;
布局组件(详细注释版)
// 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组件(详细注释版)
// src/components/Navigation.js
// 导入React和useState
import React, { useState } from 'react';
// 导入路由Hooks
import { useNavigate, useLocation, useParams } from 'react-router-dom';// 定义Navigation组件
function Navigation() {// 使用路由Hooksconst 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"><inputtype="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 动态路由参数
产品详情页面(详细注释版)
// src/pages/ProductDetail.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useNavigate, useLocation } from 'react-router-dom';// 定义ProductDetail组件
function ProductDetail() {// 使用路由Hooksconst { 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><inputtype="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 嵌套路由实现
用户设置页面(详细注释版)
// src/pages/UserSettings.js
// 导入React和useState
import React, { useState } from 'react';
// 导入路由Hooks
import { useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';// 定义UserSettings组件
function UserSettings() {// 使用路由Hooksconst { 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;
用户活动页面(详细注释版)
// src/pages/UserActivity.js
// 导入React和useState
import React, { useState, useEffect } from 'react';
// 导入路由Hooks
import { useParams, useNavigate } from 'react-router-dom';// 定义UserActivity组件
function UserActivity() {// 使用路由Hooksconst { 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 路由守卫实现
认证守卫组件(详细注释版)
// 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 }) {// 使用认证Hookconst { 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;
权限控制组件(详细注释版)
// 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
}) {// 使用认证Hookconst { hasRole, hasPermission } = useAuth();// 检查角色权限const hasRoleAccess = role ? hasRole(role) : true;// 检查具体权限const hasPermissionAccess = permission ? hasPermission(permission) : true;// 根据requireAll参数决定权限检查逻辑const hasAccess = requireAll ? hasRoleAccess && hasPermissionAccess: hasRoleAccess || hasPermissionAccess;// 如果没有权限,返回fallback或nullif (!hasAccess) {return fallback;}// 如果有权限,渲染子组件return children;
}// 导出PermissionGate组件
export default PermissionGate;
4.2 路由配置和权限
带权限的路由配置(详细注释版)
// 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;
登录页面(详细注释版)
// 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() {// 使用路由Hooksconst navigate = useNavigate();const location = useLocation();// 使用认证Hookconst { 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><inputtype="email"id="email"name="email"value={formData.email}onChange={handleChange}required/></div><div className="form-group"><label htmlFor="password">Password</label><inputtype="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;
未授权页面(详细注释版)
// src/pages/Unauthorized.js
// 导入React
import React from 'react';
// 导入路由Hooks
import { useLocation, useNavigate } from 'react-router-dom';// 定义Unauthorized组件
function Unauthorized() {// 使用路由Hooksconst 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;
第五部分:实践项目(详细注释版)
项目:完整的博客系统
主应用组件(详细注释版)
// 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;
博客列表页面(详细注释版)
// 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() {// 使用路由Hooksconst navigate = useNavigate();const [searchParams, setSearchParams] = useSearchParams();// 使用认证Hookconst { 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"><inputtype="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;
文章详情页面(详细注释版)
// 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() {// 使用路由Hooksconst { postId } = useParams();const navigate = useNavigate();const location = useLocation();// 使用认证Hookconst { 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"><textareavalue={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;
样式文件(详细注释版)
/* 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;}
}
练习题目
基础练习
- 路由基础练习
// 练习1:创建一个简单的导航系统
// 实现:首页、关于、联系、404页面
// 包含:导航栏、页脚、响应式设计// 练习2:实现动态路由
// 实现:用户详情页、产品详情页
// 包含:路由参数、查询参数、编程式导航
- 嵌套路由练习
// 练习3:实现用户管理系统的嵌套路由
// 实现:用户列表、用户详情、用户设置
// 包含:嵌套布局、Outlet组件// 练习4:实现博客系统的嵌套路由
// 实现:文章列表、文章详情、评论系统
// 包含:动态路由、状态管理
进阶练习
- 路由守卫练习
// 练习5:实现完整的权限控制系统
// 实现:登录、注册、权限验证
// 包含:路由守卫、权限控制、状态管理// 练习6:实现企业级应用的路由系统
// 实现:多级路由、权限控制、状态管理
// 包含:最佳实践、性能优化
- 综合应用练习
// 练习7:构建完整的电商系统
// 实现:商品展示、购物车、订单管理
// 包含:路由配置、状态管理、权限控制
学习检查点
完成标准
- 理解React Router基础概念
- 掌握路由配置和导航
- 学会动态路由和嵌套路由
- 掌握路由守卫和权限控制
- 完成所有练习题目
自我测试
- React Router的核心概念是什么?
- 如何实现动态路由和嵌套路由?
- 路由守卫的作用是什么?
- 如何优化路由性能?
扩展阅读
推荐资源
- React Router官方文档
- React Router教程
- 路由最佳实践
