news 2026/7/9 1:34:40

06-高级模式与实战项目——18. 实战项目二:博客系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
06-高级模式与实战项目——18. 实战项目二:博客系统

18. 实战项目二:博客系统

概述

博客系统是一个完整的全栈项目,涵盖文章列表、详情页、创建/编辑文章、评论功能等。通过这个项目,你将掌握数据获取、路由、状态管理、表单处理等核心技能。

维度内容
What完整博客系统,包含文章管理和评论功能
Why掌握数据获取、路由、状态管理
When中级实战练习
Where前后端完整项目
Who有一定基础的开发者
How实现文章列表、详情、创建、评论等

1. 项目需求

1.1 功能列表

  • ✅ 文章列表展示
  • ✅ 文章详情页面
  • ✅ 创建新文章
  • ✅ 编辑文章
  • ✅ 删除文章
  • ✅ 文章分类/标签
  • ✅ 评论功能
  • ✅ 搜索功能
  • ✅ 分页/无限滚动

1.2 技术栈

  • React 19
  • TypeScript
  • React Router v6
  • React Query (TanStack Query)
  • Tailwind CSS
  • JSON Server (Mock API)

2. 项目结构

blog-system/ ├── src/ │ ├── components/ │ │ ├── Layout/ │ │ │ ├── Header.tsx │ │ │ ├── Footer.tsx │ │ │ └── Layout.tsx │ │ ├── PostCard.tsx │ │ ├── PostForm.tsx │ │ ├── CommentList.tsx │ │ ├── CommentForm.tsx │ │ ├── SearchBar.tsx │ │ └── Pagination.tsx │ ├── pages/ │ │ ├── HomePage.tsx │ │ ├── PostPage.tsx │ │ ├── CreatePostPage.tsx │ │ ├── EditPostPage.tsx │ │ └── AboutPage.tsx │ ├── hooks/ │ │ ├── usePosts.ts │ │ ├── usePost.ts │ │ └── useComments.ts │ ├── api/ │ │ └── client.ts │ ├── types/ │ │ └── index.ts │ ├── App.tsx │ └── main.tsx ├── db.json └── package.json

3. 代码实现

3.1 类型定义

// src/types/index.ts export interface Post { id: string; title: string; content: string; excerpt: string; author: string; category: string; tags: string[]; coverImage?: string; createdAt: string; updatedAt?: string; viewCount: number; } export interface Comment { id: string; postId: string; author: string; content: string; createdAt: string; } export interface Category { id: string; name: string; slug: string; count: number; } export interface ApiResponse<T> { data: T; total?: number; page?: number; pageSize?: number; }

3.2 API 客户端

// src/api/client.ts const API_BASE = 'http://localhost:3001'; async function request<T>( endpoint: string, options?: RequestInit ): Promise<T> { const response = await fetch(`${API_BASE}${endpoint}`, { headers: { 'Content-Type': 'application/json', }, ...options, }); if (!response.ok) { throw new Error(`API Error: ${response.status}`); } return response.json(); } // Posts export const getPosts = (params?: { category?: string; search?: string; page?: number; limit?: number }) => { const queryParams = new URLSearchParams(); if (params?.category) queryParams.append('category', params.category); if (params?.search) queryParams.append('q', params.search); if (params?.page) queryParams.append('_page', String(params.page)); if (params?.limit) queryParams.append('_limit', String(params.limit)); const query = queryParams.toString(); return request<Post[]>(`/posts${query ? `?${query}` : ''}`); }; export const getPost = (id: string) => request<Post>(`/posts/${id}`); export const createPost = (post: Omit<Post, 'id' | 'createdAt' | 'viewCount'>) => request<Post>('/posts', { method: 'POST', body: JSON.stringify({ ...post, createdAt: new Date().toISOString(), viewCount: 0, }), }); export const updatePost = (id: string, post: Partial<Post>) => request<Post>(`/posts/${id}`, { method: 'PATCH', body: JSON.stringify(post), }); export const deletePost = (id: string) => request<void>(`/posts/${id}`, { method: 'DELETE' }); // Comments export const getComments = (postId: string) => request<Comment[]>(`/comments?postId=${postId}&_sort=createdAt&_order=desc`); export const createComment = (comment: Omit<Comment, 'id' | 'createdAt'>) => request<Comment>('/comments', { method: 'POST', body: JSON.stringify({ ...comment, createdAt: new Date().toISOString(), }), }); // Categories export const getCategories = () => request<Category[]>('/categories');

3.3 自定义 Hooks

// src/hooks/usePosts.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getPosts, createPost, deletePost } from '../api/client'; import { Post } from '../types'; export function usePosts(filters?: { category?: string; search?: string; page?: number }) { return useQuery({ queryKey: ['posts', filters], queryFn: () => getPosts(filters), }); } export function useCreatePost() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (post: Omit<Post, 'id' | 'createdAt' | 'viewCount'>) => createPost(post), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); } export function useDeletePost() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => deletePost(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); }
// src/hooks/usePost.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getPost, updatePost } from '../api/client'; export function usePost(id: string) { return useQuery({ queryKey: ['post', id], queryFn: () => getPost(id), enabled: !!id, }); } export function useUpdatePost() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial<Post> }) => updatePost(id, data), onSuccess: (_, { id }) => { queryClient.invalidateQueries({ queryKey: ['post', id] }); queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); }
// src/hooks/useComments.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getComments, createComment } from '../api/client'; export function useComments(postId: string) { return useQuery({ queryKey: ['comments', postId], queryFn: () => getComments(postId), enabled: !!postId, }); } export function useCreateComment(postId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (comment: Omit<Comment, 'id' | 'createdAt' | 'postId'>) => createComment({ ...comment, postId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['comments', postId] }); }, }); }

3.4 布局组件

// src/components/Layout/Header.tsx import { Link } from 'react-router-dom'; import SearchBar from '../SearchBar'; export default function Header() { return ( <header className="bg-white shadow-md"> <div className="container mx-auto px-4 py-4"> <div className="flex justify-between items-center"> <Link to="/" className="text-2xl font-bold text-blue-600"> 技术博客 </Link> <div className="flex items-center gap-6"> <SearchBar /> <Link to="/about" className="hover:text-blue-600">关于</Link> <Link to="/posts/new" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" > 写文章 </Link> </div> </div> </div> </header> ); } // src/components/Layout/Footer.tsx export default function Footer() { return ( <footer className="bg-gray-800 text-white mt-12"> <div className="container mx-auto px-4 py-8 text-center"> <p>© 2024 技术博客 - 分享知识和经验</p> </div> </footer> ); } // src/components/Layout/Layout.tsx import { Outlet } from 'react-router-dom'; import Header from './Header'; import Footer from './Footer'; export default function Layout() { return ( <div className="min-h-screen flex flex-col"> <Header /> <main className="flex-1 container mx-auto px-4 py-8"> <Outlet /> </main> <Footer /> </div> ); }

3.5 PostCard 组件

// src/components/PostCard.tsx import { Link } from 'react-router-dom'; import { Post } from '../types'; interface PostCardProps { post: Post; } export default function PostCard({ post }: PostCardProps) { return ( <article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"> {post.coverImage && ( <img src={post.coverImage} alt={post.title} className="w-full h-48 object-cover" /> )} <div className="p-6"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-2"> <span>{post.category}</span> <span>•</span> <span>{new Date(post.createdAt).toLocaleDateString()}</span> </div> <Link to={`/posts/${post.id}`}> <h2 className="text-xl font-bold mb-2 hover:text-blue-600"> {post.title} </h2> </Link> <p className="text-gray-600 mb-4">{post.excerpt}</p> <div className="flex justify-between items-center"> <span className="text-sm text-gray-500">作者: {post.author}</span> <div className="flex gap-2"> {post.tags.map(tag => ( <span key={tag} className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded"> {tag} </span> ))} </div> </div> </div> </article> ); }

3.6 页面组件

// src/pages/HomePage.tsx import { useState } from 'react'; import { usePosts } from '../hooks/usePosts'; import PostCard from '../components/PostCard'; import Pagination from '../components/Pagination'; const POSTS_PER_PAGE = 6; export default function HomePage() { const [page, setPage] = useState(1); const { data: posts, isLoading, error } = usePosts({ page, limit: POSTS_PER_PAGE }); if (isLoading) { return ( <div className="flex justify-center py-12"> <div className="text-gray-500">加载中...</div> </div> ); } if (error) { return ( <div className="text-center py-12 text-red-500"> 加载失败: {error.message} </div> ); } return ( <div> <h1 className="text-3xl font-bold mb-8">最新文章</h1> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {posts?.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> <Pagination currentPage={page} totalPages={Math.ceil((posts?.length || 0) / POSTS_PER_PAGE)} onPageChange={setPage} /> </div> ); }
// src/pages/PostPage.tsx import { useParams } from 'react-router-dom'; import { usePost } from '../hooks/usePost'; import { useComments, useCreateComment } from '../hooks/useComments'; import CommentList from '../components/CommentList'; import CommentForm from '../components/CommentForm'; export default function PostPage() { const { id } = useParams<{ id: string }>(); const { data: post, isLoading, error } = usePost(id!); const { data: comments, isLoading: commentsLoading } = useComments(id!); const createComment = useCreateComment(id!); if (isLoading) { return <div className="text-center py-12">加载中...</div>; } if (error || !post) { return <div className="text-center py-12 text-red-500">文章不存在</div>; } const handleAddComment = async (data: { author: string; content: string }) => { await createComment.mutateAsync(data); }; return ( <article className="max-w-3xl mx-auto"> <h1 className="text-4xl font-bold mb-4">{post.title}</h1> <div className="flex items-center gap-4 text-gray-500 mb-8 pb-4 border-b"> <span>作者: {post.author}</span> <span>{new Date(post.createdAt).toLocaleDateString()}</span> <span>分类: {post.category}</span> <span>阅读: {post.viewCount}</span> </div> <div className="prose max-w-none mb-8"> {post.content} </div> <div className="flex gap-2 mb-8"> {post.tags.map(tag => ( <span key={tag} className="px-3 py-1 bg-gray-100 rounded-full text-sm"> #{tag} </span> ))} </div> <section className="border-t pt-8"> <h2 className="text-2xl font-bold mb-4"> 评论 ({comments?.length || 0}) </h2> <CommentForm onSubmit={handleAddComment} isLoading={createComment.isPending} /> {commentsLoading ? ( <div className="text-center py-8">加载评论中...</div> ) : ( <CommentList comments={comments || []} /> )} </section> </article> ); }

3.7 Mock 数据 (db.json)

{"posts":[{"id":"1","title":"React 19 新特性详解","content":"React 19 带来了许多激动人心的新特性...","excerpt":"React 19 带来了许多激动人心的新特性...","author":"张三","category":"React","tags":["React","前端"],"createdAt":"2024-01-15T10:00:00Z","viewCount":1234}],"comments":[{"id":"1","postId":"1","author":"李四","content":"非常棒的文章!","createdAt":"2024-01-15T12:00:00Z"}],"categories":[{"id":"1","name":"React","slug":"react","count":10},{"id":"2","name":"TypeScript","slug":"typescript","count":8},{"id":"3","name":"前端工程化","slug":"engineering","count":6}]}

4. 项目运行

# 安装依赖npminstallreact-router-dom @tanstack/react-query# 安装 JSON Servernpminstall-gjson-server# 启动 Mock APItimeout/t5/nobreak# 或使用 Concurrencynpminstall-gconcurrently# 启动项目json-server--watchdb.json--port3001npmrun dev

5. 总结

核心知识点

知识点应用
React Router页面路由、嵌套路由
React Query数据获取、缓存、状态管理
自定义 HooksusePosts、usePost、useComments
表单处理创建/编辑文章、评论
类型安全TypeScript 完整类型定义

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 1:34:11

掌握Git rebase与merge的区别

掌握Git rebase与merge的区别&#xff1a;重塑历史与保留记录的哲学之争在版本控制系统的世界里&#xff0c;Git无疑占据了统治地位。而git rebase与git merge这两个命令&#xff0c;则是每一位Git使用者进阶路上必须深入理解的核心概念。它们都用于整合不同分支的修改&#xf…

作者头像 李华
网站建设 2026/7/9 1:33:54

NLP自然语言处理实战

NLP自然语言处理实战&#xff1a;从理论到应用的跨越 自然语言处理&#xff08;NLP&#xff09;作为人工智能皇冠上的一颗明珠&#xff0c;早已不再是实验室里的抽象概念。它已深度融入我们的日常生活&#xff1a;从智能手机的语音助手&#xff0c;到电子邮件的智能回复&#x…

作者头像 李华
网站建设 2026/7/9 1:33:55

缠论可视化分析系统:基于C++的K线结构识别与动态链接库实现

缠论可视化分析系统&#xff1a;基于C的K线结构识别与动态链接库实现 【免费下载链接】Indicator 通达信缠论可视化分析插件 项目地址: https://gitcode.com/gh_mirrors/ind/Indicator 在传统股票技术分析中&#xff0c;缠论因其复杂的结构识别和严格的数学逻辑而著称&a…

作者头像 李华
网站建设 2026/7/9 1:30:04

Mysql,使用B+树存储的索引增删改查效率(七)

结合此前我们围绕MySQL B树索引展开的相关讨论&#xff0c;使用B树存储的索引增删改查效率整体表现优异&#xff0c;是适配磁盘场景的最优索引结构之一&#xff1a; ‌查询效率‌ 时间复杂度稳定为‌O(logₘN)‌&#xff0c;千万级数据下树高仅3层左右&#xff0c;单次等值查…

作者头像 李华
网站建设 2026/7/9 1:29:01

HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法

HarmonyKit | 鸿蒙新特性应用&#xff1a;颜色转换 HEX↔RGB↔HSL 三色空间互转算法 引言 在开发者工具箱中&#xff0c;颜色格式转换是一个看似简单实则涉及较多数学细节的功能。前端开发者经常需要在 CSS 的 HEX 表示法、RGB 函数表示法和 HSL 函数表示法之间进行切换。Har…

作者头像 李华
网站建设 2026/7/9 1:28:54

Python 数据可视化之 Matplotlib——从基础到高级图表

数据可视化是数据分析中最重要的环节之一。Matplotlib 是 Python 最基础的可视化库&#xff0c;掌握它之后学习 Seaborn、Plotly 等会非常轻松。 一、基础图表 import matplotlib.pyplot as plt import numpy as np# 设置中文字体 plt.rcParams[font.sans-serif] [SimHei, Mic…

作者头像 李华