在本项目中,我们将构建一个功能完整的博客系统,包含以下主要功能:
blog-system/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ ├── PostList.jsx
│ │ ├── PostDetail.jsx
│ │ ├── CommentForm.jsx
│ │ ├── CommentList.jsx
│ │ └── Sidebar.jsx
│ ├── contexts/
│ │ └── BlogContext.jsx
│ ├── hooks/
│ │ └── useBlogData.js
│ ├── services/
│ │ └── api.js
│ ├── styles/
│ │ └── App.css
│ ├── App.jsx
│ └── index.js
└── package.json
使用Create React App创建项目:
npx create-react-app blog-system
cd blog-system
npm install react-router-dom axios
npm install json-server --save-dev
在contexts/BlogContext.jsx中创建全局状态管理:
import React, { createContext, useState, useContext, useEffect } from 'react';
import { getPosts, getCategories } from '../services/api';
const BlogContext = createContext();
export const useBlog = () => useContext(BlogContext);
export const BlogProvider = ({ children }) => {
const [posts, setPosts] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [currentPost, setCurrentPost] = useState(null);
const fetchPosts = async () => {
setLoading(true);
try {
const data = await getPosts();
setPosts(data);
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
const fetchCategories = async () => {
try {
const data = await getCategories();
setCategories(data);
} catch (error) {
console.error('Error fetching categories:', error);
}
};
const getPostById = (id) => {
return posts.find(post => post.id === parseInt(id));
};
const addComment = (postId, comment) => {
setPosts(posts.map(post => {
if (post.id === postId) {
return {
...post,
comments: [...post.comments, comment]
};
}
return post;
}));
};
useEffect(() => {
fetchPosts();
fetchCategories();
}, []);
const value = {
posts,
categories,
loading,
currentPost,
setCurrentPost,
getPostById,
addComment,
fetchPosts
};
return (
<BlogContext.Provider value={value}>
{children}
</BlogContext.Provider>
);
};
在services/api.js中配置API调用:
import axios from 'axios';
const API_BASE_URL = 'http://localhost:3001';
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
export const getPosts = async () => {
try {
const response = await api.get('/posts');
return response.data;
} catch (error) {
console.error('Error fetching posts:', error);
throw error;
}
};
export const getPostById = async (id) => {
try {
const response = await api.get(`/posts/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching post ${id}:`, error);
throw error;
}
};
export const getCategories = async () => {
try {
const response = await api.get('/categories');
return response.data;
} catch (error) {
console.error('Error fetching categories:', error);
throw error;
}
};
export const createComment = async (postId, comment) => {
try {
const response = await api.post(`/posts/${postId}/comments`, comment);
return response.data;
} catch (error) {
console.error('Error creating comment:', error);
throw error;
}
};
创建components/PostList.jsx:
import React from 'react';
import { Link } from 'react-router-dom';
import { useBlog } from '../contexts/BlogContext';
const PostList = () => {
const { posts, loading } = useBlog();
if (loading) {
return (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
return (
<div className="post-list">
{posts.map(post => (
<div key={post.id} className="card mb-4">
<div className="card-body">
<h2 className="card-title">
<Link to={`/post/${post.id}`} className="text-decoration-none">
{post.title}
</Link>
</h2>
<p className="card-text text-muted">
<small>
<i className="fas fa-calendar me-1"></i>
{new Date(post.createdAt).toLocaleDateString()}
<i className="fas fa-tag ms-3 me-1"></i>
{post.category}
</small>
</p>
<p className="card-text">{post.excerpt}</p>
<Link to={`/post/${post.id}`} className="btn btn-primary">
阅读更多
</Link>
</div>
</div>
))}
</div>
);
};
export default PostList;
创建components/PostDetail.jsx:
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useBlog } from '../contexts/BlogContext';
import CommentForm from './CommentForm';
import CommentList from './CommentList';
const PostDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { getPostById, setCurrentPost } = useBlog();
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPost = async () => {
setLoading(true);
const postData = getPostById(id);
if (postData) {
setPost(postData);
setCurrentPost(postData);
} else {
navigate('/404');
}
setLoading(false);
};
fetchPost();
}, [id, getPostById, setCurrentPost, navigate]);
if (loading) {
return (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
if (!post) {
return <div className="alert alert-danger">文章不存在</div>;
}
return (
<div className="post-detail">
<article>
<h1 className="mb-4">{post.title}</h1>
<div className="text-muted mb-4">
<span className="me-3">
<i className="fas fa-user me-1"></i>
{post.author}
</span>
<span className="me-3">
<i className="fas fa-calendar me-1"></i>
{new Date(post.createdAt).toLocaleDateString()}
</span>
<span className="badge bg-secondary">
<i className="fas fa-tag me-1"></i>
{post.category}
</span>
</div>
<div className="post-content mb-5">
{post.content}
</div>
<div className="post-tags mb-5">
{post.tags && post.tags.map(tag => (
<span key={tag} className="badge bg-light text-dark me-2">
#{tag}
</span>
))}
</div>
<hr />
<div className="comments-section mt-5">
<h3 className="mb-4">评论 ({post.comments?.length || 0})</h3>
<CommentForm postId={post.id} />
<CommentList comments={post.comments || []} />
</div>
</article>
</div>
);
};
export default PostDetail;
更新App.jsx:
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { BlogProvider } from './contexts/BlogContext';
import Header from './components/Header';
import Footer from './components/Footer';
import Sidebar from './components/Sidebar';
import PostList from './components/PostList';
import PostDetail from './components/PostDetail';
import './styles/App.css';
function App() {
return (
<BlogProvider>
<Router>
<div className="app">
<Header />
<main className="container mt-4">
<div className="row">
<div className="col-lg-8">
<Routes>
<Route path="/" element={<PostList />} />
<Route path="/post/:id" element={<PostDetail />} />
<Route path="/category/:category" element={<PostList />} />
<Route path="*" element={<div className="alert alert-warning">页面未找到</div>} />
</Routes>
</div>
<div className="col-lg-4">
<Sidebar />
</div>
</div>
</main>
<Footer />
</div>
</Router>
</BlogProvider>
);
}
export default App;
创建db.json用于JSON Server:
{
"posts": [
{
"id": 1,
"title": "React入门指南",
"author": "张三",
"category": "React",
"tags": ["react", "前端", "javascript"],
"content": "React是一个用于构建用户界面的JavaScript库...",
"excerpt": "学习React的基础知识和核心概念",
"createdAt": "2024-01-15",
"comments": [
{
"id": 1,
"author": "李四",
"content": "非常好的教程,受益匪浅!",
"createdAt": "2024-01-16"
}
]
},
{
"id": 2,
"title": "深入理解React Hooks",
"author": "王五",
"category": "React",
"tags": ["hooks", "react", "高级"],
"content": "React Hooks是React 16.8引入的新特性...",
"excerpt": "掌握React Hooks的高级用法",
"createdAt": "2024-01-14",
"comments": []
}
],
"categories": [
{"id": 1, "name": "React"},
{"id": 2, "name": "JavaScript"},
{"id": 3, "name": "CSS"}
]
}
# 启动React开发服务器
npm start
# 启动JSON Server(在另一个终端)
npx json-server --watch db.json --port 3001
通过这个实战项目,你学到了: