React实战项目:博客系统

这个实战项目将带领你使用React构建一个完整的博客系统,涵盖现代React开发的核心概念。

项目概述

在本项目中,我们将构建一个功能完整的博客系统,包含以下主要功能:

  • 文章列表展示
  • 文章详情页面
  • 文章分类和标签
  • 文章搜索功能
  • 用户评论系统
  • 响应式设计

技术栈

React 18 React Router v6 Context API React Hooks Axios Bootstrap 5 JSON Server

项目结构


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
                    

逐步实现

1 项目初始化

使用Create React App创建项目:


npx create-react-app blog-system
cd blog-system
npm install react-router-dom axios
npm install json-server --save-dev
                        

2 创建Context管理状态

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>
    );
};
                        

3 创建API服务

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;
    }
};
                        

4 文章列表组件

创建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;
                        

5 文章详情组件

创建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;
                        

6 主应用组件

更新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;
                        

7 数据库配置

创建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
                        

项目总结

通过这个实战项目,你学到了:

  1. 使用React Context进行全局状态管理
  2. React Router v6的路由配置
  3. 自定义Hook的创建和使用
  4. 组件化开发的最佳实践
  5. 与后端API的数据交互
  6. 响应式UI设计
下一步学习建议
  • 尝试添加用户认证功能
  • 实现文章的管理功能(增删改查)
  • 添加文章分页功能
  • 集成Markdown编辑器
  • 部署到云服务器