当前位置: 首页 > news >正文

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 完整类型定义

http://www.cnnetsun.cn/news/3231813.html

相关文章:

  • 掌握Git rebase与merge的区别
  • NLP自然语言处理实战
  • 缠论可视化分析系统:基于C++的K线结构识别与动态链接库实现
  • Mysql,使用B+树存储的索引增删改查效率(七)
  • HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法
  • Python 数据可视化之 Matplotlib——从基础到高级图表
  • HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计
  • HarmonyKit | 鸿蒙新特性实践:进制转换器 2/8/10/16 四进制通用互转
  • DeepSeek V3 vs Claude 3.5 Sonnet:程序员日常开发场景深度横向对比
  • SFT数据质量评估:3种自动化方法筛选千条指令数据,PPL降低20%
  • 2026抖音动图去水印免费方法,无水印保存抖音GIF技巧
  • Transformer 与 RNN/LSTM 对比:5 个维度解析长序列建模效率差异
  • YOLOv8 训练数据配置避坑:3个常见 data.yaml 错误与修复方案
  • Agent Harness 十二大模块完全解析:为什么生产级智能体的胜负手不在模型,而在“脚手架”
  • matlab和simulink的入门联合仿真-入门级!保姆级!包成功
  • 计算机大数据毕设实战-基于 Django 框架的电商用户消费行为画像分析系统的设计与实现 基于多源数据融合的电商用户精准画像可视化系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • VMware新增EXSI虚拟机
  • 围绕MonetaMarkets提醒机制看,表现如何?
  • 容器网络与存储:lxcfs-tools如何协同其他容器技术栈
  • Codex CLI一次改多个文件安全吗?提交前检查清单与PR验证流程
  • MCP 协议从零实现:打造 Agent 的“应用商店“——工具热插拔与标准化生态
  • Android 7系统网络(二)内核层—netfilter_iptables与路由策略
  • 线段树 Beats 技巧在复杂区间问题中的实战解读
  • 我和 Fable 5 聊了一晚上 AI,得出的结论大吃一惊
  • EfficientNet-B0 到 B3 复合缩放实战:4 个模型在 224x224 输入下的精度/速度权衡
  • 134.标准化 PLC 工程落地!扫描周期优化 + 防抖处理 + 状态机防死锁
  • BsMax插件:让3ds Max用户在Blender中无缝工作的终极解决方案
  • 计算机毕业设计之民办高校二手物品交易网的设计与实现
  • 如何快速集成移动端PDF预览:pdfh5.js的完整解决方案
  • 5分钟完全解锁索尼相机隐藏功能:OpenMemories-Tweak终极指南