06-高级模式与实战项目——21. 实战项目五:社交媒体
21. 实战项目五社交媒体概述社交媒体应用是一个综合性的全栈项目涵盖动态发布、点赞评论、用户关注、实时消息等核心功能。通过这个项目你将掌握实时交互、乐观更新、无限滚动等高级技能。维度内容What完整社交媒体应用包含动态发布、点赞、评论Why掌握实时交互、乐观更新、无限滚动When综合实战练习Where完整全栈项目Who需要复杂交互经验的开发者How实现动态流、点赞、评论、用户关注等1. 项目需求1.1 功能列表✅ 用户注册/登录✅ 动态发布文字/图片✅ 动态列表无限滚动✅ 点赞/取消点赞乐观更新✅ 评论功能✅ 关注/取消关注用户✅ 个人主页✅ 消息通知✅ 搜索用户1.2 技术栈React 19TypeScriptReact Router v6Zustand (状态管理)React Query (数据获取)Tailwind CSSJSON Server (Mock API)2. 项目结构social-media/ ├── src/ │ ├── components/ │ │ ├── Layout/ │ │ │ ├── Header.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── Layout.tsx │ │ ├── PostCard.tsx │ │ ├── PostForm.tsx │ │ ├── CommentList.tsx │ │ ├── CommentForm.tsx │ │ ├── UserCard.tsx │ │ ├── NotificationBell.tsx │ │ └── InfiniteScroll.tsx │ ├── pages/ │ │ ├── HomePage.tsx │ │ ├── ProfilePage.tsx │ │ ├── ExplorePage.tsx │ │ ├── NotificationsPage.tsx │ │ ├── LoginPage.tsx │ │ └── RegisterPage.tsx │ ├── stores/ │ │ ├── authStore.ts │ │ └── notificationStore.ts │ ├── hooks/ │ │ ├── usePosts.ts │ │ ├── usePost.ts │ │ ├── useComments.ts │ │ ├── useUser.ts │ │ └── useInfiniteScroll.ts │ ├── api/ │ │ └── client.ts │ ├── types/ │ │ └── index.ts │ ├── App.tsx │ └── main.tsx ├── db.json └── package.json3. 代码实现3.1 类型定义// src/types/index.ts export interface User { id: string; username: string; name: string; email: string; avatar: string; bio: string; followersCount: number; followingCount: number; postsCount: number; isFollowed?: boolean; } export interface Post { id: string; userId: string; user: User; content: string; images: string[]; likesCount: number; commentsCount: number; isLiked?: boolean; createdAt: string; } export interface Comment { id: string; postId: string; userId: string; user: User; content: string; createdAt: string; } export interface Notification { id: string; userId: string; type: like | comment | follow; fromUser: User; postId?: string; read: boolean; createdAt: string; }3.2 状态管理// src/stores/authStore.ts import { create } from zustand; import { persist } from zustand/middleware; import { User } from ../types; interface AuthState { user: User | null; token: string | null; isLoading: boolean; login: (email: string, password: string) Promisevoid; register: (username: string, email: string, password: string) Promisevoid; logout: () void; updateUser: (updates: PartialUser) void; } export const useAuthStore createAuthState()( persist( (set) ({ user: null, token: null, isLoading: false, login: async (email, password) { set({ isLoading: true }); // 模拟登录 await new Promise(resolve setTimeout(resolve, 1000)); set({ user: { id: 1, username: john, name: John Doe, email, avatar: , bio: , followersCount: 0, followingCount: 0, postsCount: 0 }, token: fake-token, isLoading: false, }); }, register: async (username, email, password) { set({ isLoading: true }); await new Promise(resolve setTimeout(resolve, 1000)); set({ user: { id: 1, username, name: username, email, avatar: , bio: , followersCount: 0, followingCount: 0, postsCount: 0 }, token: fake-token, isLoading: false, }); }, logout: () set({ user: null, token: null }), updateUser: (updates) set((state) ({ user: state.user ? { ...state.user, ...updates } : null, })), }), { name: auth } ) );3.3 自定义 Hooks// src/hooks/usePosts.ts import { useInfiniteQuery, useMutation, useQueryClient } from tanstack/react-query; import { Post } from ../types; const fetchPosts async ({ pageParam 1 }) { const response await fetch(/api/posts?_page${pageParam}_limit10_sortcreatedAt_orderdesc); const data await response.json(); return { data, nextPage: data.length 10 ? pageParam 1 : undefined, }; }; export function usePosts() { return useInfiniteQuery({ queryKey: [posts], queryFn: fetchPosts, initialPageParam: 1, getNextPageParam: (lastPage) lastPage.nextPage, }); } export function useLikePost() { const queryClient useQueryClient(); return useMutation({ mutationFn: async ({ postId, isLiked }: { postId: string; isLiked: boolean }) { const method isLiked ? DELETE : POST; await fetch(/api/posts/${postId}/like, { method }); }, onMutate: async ({ postId, isLiked }) { await queryClient.cancelQueries({ queryKey: [posts] }); const previousPosts queryClient.getQueryData([posts]); queryClient.setQueryData([posts], (old: any) { if (!old) return old; return { ...old, pages: old.pages.map((page: any) ({ ...page, data: page.data.map((post: Post) post.id postId ? { ...post, isLiked: !isLiked, likesCount: isLiked ? post.likesCount - 1 : post.likesCount 1, } : post ), })), }; }); return { previousPosts }; }, onError: (err, variables, context) { queryClient.setQueryData([posts], context?.previousPosts); }, }); } // src/hooks/useComments.ts import { useQuery, useMutation, useQueryClient } from tanstack/react-query; export function useComments(postId: string) { return useQuery({ queryKey: [comments, postId], queryFn: () fetch(/api/comments?postId${postId}_sortcreatedAt_orderasc).then(res res.json()), enabled: !!postId, }); } export function useCreateComment(postId: string) { const queryClient useQueryClient(); return useMutation({ mutationFn: (content: string) fetch(/api/comments, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ postId, content, userId: 1 }), }).then(res res.json()), onSuccess: () { queryClient.invalidateQueries({ queryKey: [comments, postId] }); queryClient.invalidateQueries({ queryKey: [posts] }); }, }); }3.4 PostCard 组件// src/components/PostCard.tsx import { useState } from react; import { Link } from react-router-dom; import { Post } from ../types; import { useLikePost } from ../hooks/usePosts; import { formatDistanceToNow } from date-fns; import { zhCN } from date-fns/locale; import CommentList from ./CommentList; import CommentForm from ./CommentForm; interface PostCardProps { post: Post; } export default function PostCard({ post }: PostCardProps) { const [showComments, setShowComments] useState(false); const likeMutation useLikePost(); const handleLike () { likeMutation.mutate({ postId: post.id, isLiked: !!post.isLiked }); }; return ( div classNamebg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-4 {/* 用户信息 */} div classNameflex items-center gap-3 mb-3 Link to{/profile/${post.user.id}} img src{post.user.avatar || https://via.placeholder.com/40} alt{post.user.name} classNamew-10 h-10 rounded-full / /Link div Link to{/profile/${post.user.id}} classNamefont-semibold hover:underline {post.user.name} /Link p classNametext-xs text-gray-500 {formatDistanceToNow(new Date(post.createdAt), { addSuffix: true, locale: zhCN })} /p /div /div {/* 内容 */} p classNamemb-3{post.content}/p {/* 图片 */} {post.images.length 0 ( div className{grid gap-2 mb-3 ${post.images.length 1 ? grid-cols-1 : grid-cols-2}} {post.images.map((img, i) ( img key{i} src{img} alt classNamerounded-lg w-full aspect-square object-cover / ))} /div )} {/* 操作按钮 */} div classNameflex gap-6 border-t border-b py-2 my-2 button onClick{handleLike} className{flex items-center gap-1 ${post.isLiked ? text-red-500 : text-gray-500} hover:text-red-500} span❤️/span span{post.likesCount}/span /button button onClick{() setShowComments(!showComments)} classNameflex items-center gap-1 text-gray-500 hover:text-blue-500 span/span span{post.commentsCount}/span /button /div {/* 评论区域 */} {showComments ( div classNamemt-3 CommentForm postId{post.id} / CommentList postId{post.id} / /div )} /div ); }3.5 PostForm 组件// src/components/PostForm.tsx import { useState } from react; import { useQueryClient } from tanstack/react-query; import { useAuthStore } from ../stores/authStore; interface PostFormProps { onSuccess?: () void; } export default function PostForm({ onSuccess }: PostFormProps) { const { user } useAuthStore(); const [content, setContent] useState(); const [isLoading, setIsLoading] useState(false); const queryClient useQueryClient(); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); if (!content.trim()) return; setIsLoading(true); // 乐观更新 const newPost { id: Date.now().toString(), userId: user!.id, user: user!, content, images: [], likesCount: 0, commentsCount: 0, isLiked: false, createdAt: new Date().toISOString(), }; queryClient.setQueryData([posts], (old: any) { if (!old) return { pages: [{ data: [newPost] }] }; return { ...old, pages: [{ data: [newPost, ...old.pages[0].data] }, ...old.pages.slice(1)], }; }); setContent(); try { await fetch(/api/posts, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ content, userId: user!.id }), }); onSuccess?.(); } catch (error) { console.error(发布失败, error); } finally { setIsLoading(false); } }; return ( form onSubmit{handleSubmit} classNamebg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-4 div classNameflex gap-3 img src{user?.avatar || https://via.placeholder.com/40} alt classNamew-10 h-10 rounded-full / textarea value{content} onChange{(e) setContent(e.target.value)} placeholder分享你的想法... classNameflex-1 p-3 border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 rows{3} / /div div classNameflex justify-end mt-3 button typesubmit disabled{!content.trim() || isLoading} classNamepx-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 {isLoading ? 发布中... : 发布} /button /div /form ); }3.6 主页组件// src/pages/HomePage.tsx import { useRef, useCallback } from react; import { usePosts } from ../hooks/usePosts; import PostCard from ../components/PostCard; import PostForm from ../components/PostForm; export default function HomePage() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } usePosts(); const observerRef useRefIntersectionObserver | null(null); const lastPostRef useCallback( (node: HTMLDivElement | null) { if (isLoading) return; if (observerRef.current) observerRef.current.disconnect(); observerRef.current new IntersectionObserver((entries) { if (entries[0].isIntersecting hasNextPage !isFetchingNextPage) { fetchNextPage(); } }); if (node) observerRef.current.observe(node); }, [isLoading, hasNextPage, isFetchingNextPage, fetchNextPage] ); if (isLoading) { return div classNametext-center py-12加载中.../div; } const posts data?.pages.flatMap(page page.data) || []; return ( div classNamemax-w-2xl mx-auto PostForm / {posts.map((post, index) ( div key{post.id} ref{index posts.length - 1 ? lastPostRef : undefined} PostCard post{post} / /div ))} {isFetchingNextPage ( div classNametext-center py-4 text-gray-500加载更多.../div )} {!hasNextPage posts.length 0 ( div classNametext-center py-4 text-gray-500没有更多了/div )} /div ); }3.7 个人主页// src/pages/ProfilePage.tsx import { useParams } from react-router-dom; import { useUser, useFollowUser } from ../hooks/useUser; import { usePostsByUser } from ../hooks/usePosts; import PostCard from ../components/PostCard; import { useAuthStore } from ../stores/authStore; export default function ProfilePage() { const { id } useParams(); const { user: currentUser } useAuthStore(); const { data: user, isLoading: userLoading } useUser(id!); const { data: posts, isLoading: postsLoading } usePostsByUser(id!); const followMutation useFollowUser(); const isOwnProfile currentUser?.id id; if (userLoading) return div classNametext-center py-12加载中.../div; if (!user) return div classNametext-center py-12用户不存在/div; return ( div classNamemax-w-2xl mx-auto {/* 用户信息 */} div classNamebg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-4 div classNameflex items-center gap-6 img src{user.avatar || https://via.placeholder.com/100} alt{user.name} classNamew-24 h-24 rounded-full / div classNameflex-1 h1 classNametext-2xl font-bold{user.name}/h1 p classNametext-gray-500{user.username}/p p classNamemt-2{user.bio}/p div classNameflex gap-4 mt-3 text-sm spanstrong{user.postsCount}/strong 帖子/span spanstrong{user.followersCount}/strong 粉丝/span spanstrong{user.followingCount}/strong 关注/span /div /div {!isOwnProfile ( button onClick{() followMutation.mutate(user.id)} className{px-4 py-2 rounded-lg ${ user.isFollowed ? bg-gray-200 text-gray-700 hover:bg-gray-300 : bg-blue-600 text-white hover:bg-blue-700 }} {user.isFollowed ? 已关注 : 关注} /button )} /div /div {/* 帖子列表 */} {postsLoading ? ( div classNametext-center py-12加载中.../div ) : ( posts?.map(post PostCard key{post.id} post{post} /) )} /div ); }3.8 无限滚动 Hook// src/hooks/useInfiniteScroll.ts import { useEffect, useRef, useCallback } from react; export function useInfiniteScroll( isLoading: boolean, hasNextPage: boolean | undefined, fetchNextPage: () void ) { const observerRef useRefIntersectionObserver | null(null); const loadMoreRef useCallback( (node: HTMLDivElement | null) { if (isLoading) return; if (observerRef.current) observerRef.current.disconnect(); observerRef.current new IntersectionObserver((entries) { if (entries[0].isIntersecting hasNextPage) { fetchNextPage(); } }); if (node) observerRef.current.observe(node); }, [isLoading, hasNextPage, fetchNextPage] ); return loadMoreRef; }3.9 Mock 数据// db.json{users:[{id:1,username:john,name:John Doe,email:johnexample.com,avatar:,bio:前端开发者,followersCount:120,followingCount:85,postsCount:15}],posts:[{id:1,userId:1,content:React 19 真是太棒了,images:[],likesCount:45,commentsCount:12,createdAt:2024-06-15T10:00:00Z}],comments:[],likes:[],follows:[]}4. 项目运行# 安装依赖npminstalltanstack/react-query date-fns# 启动项目npmrun dev5. 总结核心知识点知识点应用无限滚动动态流加载乐观更新点赞、发布动态实时交互评论、关注状态管理Zustand React Query用户认证登录/注册 阶段六全部完成完成统计分类文档数量状态高级组件模式4篇✅设计模式4篇✅测试4篇✅TypeScript 集成4篇✅实战项目5篇✅总计21篇✅ 全部完成

相关新闻

【Bug已解决】Claude Code freezes on startup / Hangs at initialization — Claude Code 启动冻结解决方案

【Bug已解决】Claude Code freezes on startup / Hangs at initialization — Claude Code 启动冻结解决方案

【Bug已解决】Claude Code freezes on startup / Hangs at initialization — Claude Code 启动冻结解决方案 1. 问题描述 运行 claude 命令后,终端卡在初始化阶段,长时间无响应: $ claude # 光标闪烁但没有任何输出 # 等待 30 秒以上仍无响应…

2026/7/9 1:29:42阅读更多 →
Cas:1260247-50-4,Biotin-PEG3-SS-NH2,生物素-聚乙二醇-双硫键-氨基

Cas:1260247-50-4,Biotin-PEG3-SS-NH2,生物素-聚乙二醇-双硫键-氨基

基本信息中文名称:生物素-三聚乙二醇-二硫键-氨基英文名称:Biotin-PEG3-SS-NH2CAS号:1260247-50-4分子式:C₂₃H₄₃N₅O₆S₃分子量:581.8纯度标准:≥95%储存条件:-20℃避光保存,避…

2026/7/9 1:29:42阅读更多 →
SpringBoot 接口幂等性设计——防重复提交终极方案

SpringBoot 接口幂等性设计——防重复提交终极方案

幂等性是指同一操作多次执行和一次执行的结果相同。支付、下单、转账等接口必须保证幂等性,否则用户多次点击就会产生多笔订单。 一、什么是幂等 非幂等:每次执行都会改变状态请求1:扣款100元 → 余额900请求2:扣款100元 → 余额…

2026/7/9 1:29:42阅读更多 →
AI 预测模型监控:数据漂移检测比精度下降更早暴露问题

AI 预测模型监控:数据漂移检测比精度下降更早暴露问题

AI 预测模型监控:数据漂移检测比精度下降更早暴露问题线上模型从 95% 准确率掉到 82%,等发现的时候已经影响了两周的业务决策。回头查数据才发现,输入特征分布从第 3 天就开始偏移了 —- 如果早做数据漂移检测,根本不用等到精度崩…

2026/7/9 2:24:46阅读更多 →
高性能服务器集群运行大模型

高性能服务器集群运行大模型

登录管理端 xshell xFtp 检查模块 -bash-4.2$ module avail--------------------------------------------- /public/apps/modulefiles/mpi --------------------------------------------- intelmpi/2023.2.0 openmpi/1.6.4 openmpi/4.0.2----------------------------…

2026/7/9 2:24:46阅读更多 →
win下yuv基于opengl的基础美颜demo

win下yuv基于opengl的基础美颜demo

本demo主要基于opengl开发&#xff0c;对yuv420视频文件进行基础美颜处理。比如实现磨皮,增白,色温,LUT风格等效果 资源链接&#xff1a;https://download.csdn.net/download/kaifu/93089977 用法&#xff1a; Usage: beauty.exe <input.yuv> <output.yuv> <wid…

2026/7/9 2:24:46阅读更多 →
Codex 实战:用真实问题串起路线

Codex 实战:用真实问题串起路线

聊《Codex 实战&#xff1a;一次新的项目切入》之前&#xff0c;先说一句实在的&#xff1a;别急着背概念&#xff0c;先看它在真实项目里到底解决什么问题。摘要先把这篇文章的目标说清楚&#xff1a;看完之后&#xff0c;你应该能判断这件事值不值得做&#xff0c;以及从哪里…

2026/7/9 2:24:46阅读更多 →
告别“缺少DLL“弹窗:3分钟搞定Windows程序运行环境配置

告别“缺少DLL“弹窗:3分钟搞定Windows程序运行环境配置

告别"缺少DLL"弹窗&#xff1a;3分钟搞定Windows程序运行环境配置 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾在打开某个游戏或软件时&am…

2026/7/9 2:24:46阅读更多 →
数据库期末复习(一)

数据库期末复习(一)

数据库期末复习&#xff08;一&#xff09; 基本概念 DBMS DBMS&#xff08;数据库管理系统&#xff09;是用户/应用程序与数据库之间的接口。应用程序通过DBMS访问数据&#xff0c;用户也通过DBMS操作数据。 DDL和DML DDL&#xff08;数据定义语言&#xff09;&#xff1a;用于…

2026/7/9 2:19:45阅读更多 →
从GitHub安全案例解析常见漏洞与防护实践

从GitHub安全案例解析常见漏洞与防护实践

1. 项目概述&#xff1a;从GitHub Trending看安全实战 最近在GitHub Trending上看到一个项目&#xff0c;叫 skills4/skills &#xff0c;它因为一些安全漏洞案例被大家讨论。这其实是一个挺典型的场景&#xff1a;一个旨在展示或教授某种技能的仓库&#xff0c;本身却成了安…

2026/7/8 5:12:14阅读更多 →
MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

# MLT 2026启示&#xff1a;因果推理与概率建模驱动下一代LLM应用## 一、背景与挑战&#xff1a;从“黑箱预测”到“可信推理”2026年6月&#xff0c;第7届机器学习与趋势国际会议&#xff08;MLT 2026&#xff09;将在悉尼召开。会议议程中&#xff0c;“因果与可解释机器学习…

2026/7/8 7:00:12阅读更多 →
通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

1. 项目概述与漏洞背景最近在梳理一些历史OA系统的安全风险时&#xff0c;通达OA v11.6版本中的一个老漏洞又进入了我的视线。这个漏洞位于/general/bi_design/appcenter/report_bi.func.php文件中&#xff0c;是一个典型的SQL注入点。虽然这个漏洞的利用方式看起来并不复杂&am…

2026/7/8 2:26:06阅读更多 →
Three.js 着色器光效教程

Three.js 着色器光效教程

着色器光效 Shader Light ▶ 在线运行案例 案例合集&#xff1a; 三维可视化功能案例&#xff08;threehub.cn&#xff09;开源仓库github地址&#xff1a; https://github.com/z2586300277/three-cesium-examples400个案例代码: 网盘链接 你将学到什么 ShaderMaterial 自…

2026/7/9 0:04:37阅读更多 →
如何5分钟掌握CS2智能库存管理:开源工具CASEMOVE终极指南

如何5分钟掌握CS2智能库存管理:开源工具CASEMOVE终极指南

如何5分钟掌握CS2智能库存管理&#xff1a;开源工具CASEMOVE终极指南 【免费下载链接】casemove A dedicated desktop app that enables you to move items in and out of storage units in CS2. 项目地址: https://gitcode.com/gh_mirrors/ca/casemove 还在为CS2存储单…

2026/7/9 0:04:37阅读更多 →
GBase 8a vs MySQL 8.0:ALTER TABLE语法与限制的5点关键差异对比

GBase 8a vs MySQL 8.0:ALTER TABLE语法与限制的5点关键差异对比

GBase 8a与MySQL 8.0&#xff1a;ALTER TABLE语法差异深度解析与实战指南1. 两种数据库的ALTER TABLE能力全景对比在数据库架构设计和运维过程中&#xff0c;表结构变更&#xff08;DDL操作&#xff09;是不可避免的需求。GBase 8a作为国产分析型数据库代表&#xff0c;与开源M…

2026/7/9 0:04:37阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时&#xff0c;发现推理速度只有可怜的 1-2 FPS&#xff0c;而别人的演示视频却能跑到 30 FPS 以上&#xff0c;那么问题很可能不在模型本身&#xff0c;而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后&#xff0c;会直接使用官方示例…

2026/7/8 6:59:54阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一&#xff1a;为什么你需要了解 Coze 和 Dify&#xff1f;如果你对 AI 应用开发感兴趣&#xff0c;但一看到“大模型”、“智能体”、“工作流”这些词就头疼&#xff0c;觉得门槛太高&#xff0c;那这篇文章就是为你准备的。很多开发者&#xff0c;包括我自己&#…

2026/7/8 13:42:39阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会&#xff1a;配图一直是个让人头疼的问题。2026年&#xff0c;AI生图工具已经非常成熟了&#xff0c;但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1&#xff1a;速度之王2026年6月11日&#xff0c…

2026/7/8 13:42:39阅读更多 →