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

📅 2026/7/9 1:34:55
06-高级模式与实战项目——18. 实战项目二:博客系统
18. 实战项目二博客系统概述博客系统是一个完整的全栈项目涵盖文章列表、详情页、创建/编辑文章、评论功能等。通过这个项目你将掌握数据获取、路由、状态管理、表单处理等核心技能。维度内容What完整博客系统包含文章管理和评论功能Why掌握数据获取、路由、状态管理When中级实战练习Where前后端完整项目Who有一定基础的开发者How实现文章列表、详情、创建、评论等1. 项目需求1.1 功能列表✅ 文章列表展示✅ 文章详情页面✅ 创建新文章✅ 编辑文章✅ 删除文章✅ 文章分类/标签✅ 评论功能✅ 搜索功能✅ 分页/无限滚动1.2 技术栈React 19TypeScriptReact Router v6React Query (TanStack Query)Tailwind CSSJSON 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.json3. 代码实现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 ApiResponseT { data: T; total?: number; page?: number; pageSize?: number; }3.2 API 客户端// src/api/client.ts const API_BASE http://localhost:3001; async function requestT( endpoint: string, options?: RequestInit ): PromiseT { 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 requestPost[](/posts${query ? ?${query} : }); }; export const getPost (id: string) requestPost(/posts/${id}); export const createPost (post: OmitPost, id | createdAt | viewCount) requestPost(/posts, { method: POST, body: JSON.stringify({ ...post, createdAt: new Date().toISOString(), viewCount: 0, }), }); export const updatePost (id: string, post: PartialPost) requestPost(/posts/${id}, { method: PATCH, body: JSON.stringify(post), }); export const deletePost (id: string) requestvoid(/posts/${id}, { method: DELETE }); // Comments export const getComments (postId: string) requestComment[](/comments?postId${postId}_sortcreatedAt_orderdesc); export const createComment (comment: OmitComment, id | createdAt) requestComment(/comments, { method: POST, body: JSON.stringify({ ...comment, createdAt: new Date().toISOString(), }), }); // Categories export const getCategories () requestCategory[](/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: OmitPost, 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: PartialPost }) 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: OmitComment, 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 classNamebg-white shadow-md div classNamecontainer mx-auto px-4 py-4 div classNameflex justify-between items-center Link to/ classNametext-2xl font-bold text-blue-600 技术博客 /Link div classNameflex items-center gap-6 SearchBar / Link to/about classNamehover:text-blue-600关于/Link Link to/posts/new classNamepx-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 classNamebg-gray-800 text-white mt-12 div classNamecontainer 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 classNamemin-h-screen flex flex-col Header / main classNameflex-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 classNamebg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow {post.coverImage ( img src{post.coverImage} alt{post.title} classNamew-full h-48 object-cover / )} div classNamep-6 div classNameflex 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 classNametext-xl font-bold mb-2 hover:text-blue-600 {post.title} /h2 /Link p classNametext-gray-600 mb-4{post.excerpt}/p div classNameflex justify-between items-center span classNametext-sm text-gray-500作者: {post.author}/span div classNameflex gap-2 {post.tags.map(tag ( span key{tag} classNamepx-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 classNameflex justify-center py-12 div classNametext-gray-500加载中.../div /div ); } if (error) { return ( div classNametext-center py-12 text-red-500 加载失败: {error.message} /div ); } return ( div h1 classNametext-3xl font-bold mb-8最新文章/h1 div classNamegrid 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 classNametext-center py-12加载中.../div; } if (error || !post) { return div classNametext-center py-12 text-red-500文章不存在/div; } const handleAddComment async (data: { author: string; content: string }) { await createComment.mutateAsync(data); }; return ( article classNamemax-w-3xl mx-auto h1 classNametext-4xl font-bold mb-4{post.title}/h1 div classNameflex 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 classNameprose max-w-none mb-8 {post.content} /div div classNameflex gap-2 mb-8 {post.tags.map(tag ( span key{tag} classNamepx-3 py-1 bg-gray-100 rounded-full text-sm #{tag} /span ))} /div section classNameborder-t pt-8 h2 classNametext-2xl font-bold mb-4 评论 ({comments?.length || 0}) /h2 CommentForm onSubmit{handleAddComment} isLoading{createComment.isPending} / {commentsLoading ? ( div classNametext-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 dev5. 总结核心知识点知识点应用React Router页面路由、嵌套路由React Query数据获取、缓存、状态管理自定义 HooksusePosts、usePost、useComments表单处理创建/编辑文章、评论类型安全TypeScript 完整类型定义