Project Templates
Social Network
您的社交网络的动态和互动
36 分
介绍 在本教程中,您将学习如何使用 back4app 作为后端服务构建具有互动功能的社交网络动态,例如帖子、评论和点赞。这些功能构成了任何社交网络应用程序的核心,使用户能够分享内容并与其他用户互动。 back4app 是一个基于 parse server 的后端即服务(baas)平台,为开发人员提供了一个强大的基础架构,以便在不管理服务器端代码的情况下创建可扩展的应用程序。其实时数据库功能和内置用户管理使其成为开发社交网络应用程序的绝佳选择。 到本教程结束时,您将实现一个功能齐全的社交动态,允许用户: 创建带有文本和图像的帖子 在动态中查看其他用户的帖子 点赞帖子 对帖子进行评论 查看带有评论的帖子详情 您将学习如何有效地构建数据库类,实现实时更新,并创建一个响应式用户界面,以便在各种设备上提供无缝体验。 先决条件 要完成本教程,您需要: 一个 back4app 账户。您可以在 back4app com https //www back4app com 一个 back4app 项目设置。您可以通过遵循我们的 back4app 入门指南 https //www back4app com/docs/get started/welcome 在您的本地计算机上安装 node js。 对 javascript 和 react js 的基本知识。 一个有效的身份验证系统。如果您还没有设置,请先遵循我们的 社交网络的身份验证系统 教程。 熟悉现代网页开发概念(组件、状态管理等)。 back4gram 项目: 在 这里 找到完整的代码, 社交网络示例项目 使用 back4app 构建。 步骤 1 – 理解数据模型 在编写任何代码之前,了解我们社交网络的数据模型是很重要的。我们需要几个 parse 类来构建我们的动态功能: 帖子类 该 帖子 类将存储所有与帖子相关的信息: 作者 指向创建帖子的用户的指针 内容 帖子的文本内容 图片 可选的图像文件 点赞 表示帖子有多少个赞的数字 评论 指向评论对象的指针数组(可选,可以单独查询) 创建时间 由 back4app 自动管理 更新时间 由 back4app 自动管理 评论类 该 评论 类将存储对帖子的评论: 作者 指向撰写评论的用户 帖子 指向被评论的帖子 内容 评论的文本内容 创建时间 由back4app自动管理 更新时间 由back4app自动管理 点赞追踪 我们有两种选项来追踪点赞: 简单计数器 在每个帖子上存储点赞计数(更简单但不够详细) 点赞记录 创建一个单独的 点赞 类来追踪每个单独的点赞(更详细) 在本教程中,我们将实现这两种方法,首先从简单的计数器开始,然后展示如何实现单独的点赞记录以获得更高级的功能。 让我们开始在back4app中设置这些类。 步骤 2 – 在 back4app 中设置数据模型 在此步骤中,您将创建 back4app 数据库中所需的类。 创建帖子类 登录到您的 back4app 控制面板并导航到您的项目。 在左侧边栏中,点击 "数据库" 以打开数据库浏览器。 点击页面顶部的 "创建类" 按钮。 在出现的模态框中,输入 "post" 作为类名,并选择 "自定义" 作为类型。然后点击 "创建类"。 \[image back4app 创建类模态框,类名输入为 "post"] 现在,向你的 post 类添加以下列: 点击 "添加列" 创建以下列: 作者 (类型:指针,目标类: user) 内容 (类型:字符串) 图片 (类型:文件) 点赞 (类型:数字,默认值:0) 创建评论类 再次点击 "创建类"。 输入 "comment" 作为类名,并选择 "custom" 作为类型。 添加以下列: 作者 (类型:指针,目标类: user) 帖子 (类型:指针,目标类:post) 内容 (类型:字符串) 设置类级别权限 为了安全起见,让我们为我们的类设置适当的权限: 在数据库浏览器中,选择post类。 点击“安全”按钮。 配置权限: 公开读取:是(任何人都可以查看帖子) 公开写入:否(只有经过身份验证的用户可以创建帖子) 公开添加字段:否 查找、获取、创建、更新、删除:需要用户身份验证 对comment类重复类似的设置。 现在我们已经设置好数据模型,让我们为社交网络动态实现前端组件。 信息流页面是任何社交网络的核心。它显示用户的帖子并允许互动。让我们创建一个获取和显示帖子的 feed 组件。 创建一个名为 src/pages/feedpage js import react, { usestate, useeffect, useref } from 'react'; import { usenavigate } from 'react router dom'; import { box, flex, vstack, heading, text, button, input, simplegrid, avatar, hstack, iconbutton, textarea, spinner, center, image, } from '@chakra ui/react'; import { link as routerlink } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function feedpage() { const \[searchquery, setsearchquery] = usestate(''); const \[newpostcontent, setnewpostcontent] = usestate(''); const \[posts, setposts] = usestate(\[]); const \[isloading, setisloading] = usestate(true); const \[isposting, setisposting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // new state variables for image upload const \[selectedimage, setselectedimage] = usestate(null); const \[imagepreview, setimagepreview] = usestate(null); const fileinputref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { // redirect to login if not authenticated navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // handle image selection const handleimagechange = (e) => { if (e target files && e target files\[0]) { const file = e target files\[0]; setselectedimage(file); // create a preview url const reader = new filereader(); reader onloadend = () => { setimagepreview(reader result); }; reader readasdataurl(file); } }; // clear selected image const handleclearimage = () => { setselectedimage(null); setimagepreview(null); if (fileinputref current) { fileinputref current value = ''; } }; // fetch posts useeffect(() => { const fetchposts = async () => { if (!currentuser) { console log('no current user, skipping post fetch'); return; } setisloading(true); console log('fetching posts for user ', currentuser id); try { // create a query for the post class const query = new parse query('post'); console log('created post query'); // include the user who created the post query include('author'); console log('including author in query'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query console log('executing query '); const results = await query find(); console log('query results received ', results length, 'posts found'); // convert parse objects to plain objects const fetchedposts = \[]; for (let i = 0; i < results length; i++) { const post = results\[i]; try { const author = post get('author'); console log(`processing post ${i+1}/${results length}, author `, author ? author id 'null'); if (!author) { console warn(`post ${post id} has no author, skipping`); continue; } const postobj = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; fetchedposts push(postobj); } catch (posterror) { console error(`error processing post ${i+1} `, posterror); console error('post data ', post tojson()); } } console log('successfully processed', fetchedposts length, 'posts'); setposts(fetchedposts); } catch (error) { console error('error fetching posts ', error); console error('error details ', error code, error message); toaster create({ title 'error loading posts', description error message, type 'error', }); } finally { setisloading(false); console log('post loading completed'); } }; fetchposts(); }, \[currentuser, navigate]); // function to create a new post with image const handlecreatepost = async () => { if (!newpostcontent trim() && !selectedimage) return; setisposting(true); console log('creating new post '); try { // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { console log('uploading image '); const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); console log('image uploaded successfully'); } console log('post object created, saving to database '); // save the post const savedpost = await post save(); console log('post saved successfully with id ', savedpost id); // add the new post to the state const newpost = { id savedpost id, content savedpost get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, image savedpost get('image') ? savedpost get('image') url() null, likes 0, createdat savedpost get('createdat') }; console log('adding new post to state'); setposts(\[newpost, posts]); setnewpostcontent(''); setselectedimage(null); setimagepreview(null); toaster create({ title 'post created', description 'your post has been published successfully!', type 'success', }); } catch (error) { console error('error creating post ', error); console error('error details ', error code, error message); toaster create({ title 'error creating post', description error message, type 'error', }); } finally { setisposting(false); console log('post creation completed'); } }; // function to like a post const handlelikepost = async (postid) => { try { // get the post const query = new parse query('post'); const post = await query get(postid); // increment likes post increment('likes'); await post save(); // update the post in the state setposts(posts map(p => { if (p id === postid) { return { p, likes p likes + 1 }; } return p; })); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // function to logout const handlelogout = async () => { try { await parse user logout(); navigate('/login'); } catch (error) { console error('error logging out ', error); } }; const handlesearchsubmit = (e) => { e preventdefault(); if (searchquery trim()) { navigate(`/search?q=${encodeuricomponent(searchquery)}`); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; return ( \<flex direction="row" h="100vh"> {/ left sidebar (navigation) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderright="1px solid" bordercolor="gray 600" \> \<vstack align="stretch" spacing={4}> \<heading size="md">social network\</heading> \<button as={routerlink} to="/feed" variant="ghost" justifycontent="flex start"> home \</button> \<button as={routerlink} to="/search" variant="ghost" justifycontent="flex start"> search \</button> \<button as={routerlink} to="/messages" variant="ghost" justifycontent="flex start"> messages \</button> \<button as={routerlink} to="/profile" variant="ghost" justifycontent="flex start"> profile \</button> \<button onclick={handlelogout} variant="ghost" colorscheme="red" justifycontent="flex start"> logout \</button> \</vstack> \</box> {/ main content (feed) /} \<box flex="1" p={4} overflowy="auto"> \<form onsubmit={handlesearchsubmit}> \<input placeholder="search " value={searchquery} onchange={(e) => setsearchquery(e target value)} mb={4} /> \</form> {/ create post with image upload /} \<box border="1px solid" bordercolor="gray 600" p={4} borderradius="md" mb={6}> \<textarea placeholder="what's on your mind?" value={newpostcontent} onchange={(e) => setnewpostcontent(e target value)} mb={2} resize="none" /> {/ image preview /} {imagepreview && ( \<box position="relative" mb={2}> \<image src={imagepreview} alt="preview" maxh="200px" borderradius="md" /> \<button position="absolute" top="2" right="2" size="sm" colorscheme="red" onclick={handleclearimage} \> remove \</button> \</box> )} \<box> \<input type="file" accept="image/ " onchange={handleimagechange} style={{ display 'none' }} ref={fileinputref} id="image upload" /> \<button as="label" htmlfor="image upload" cursor="pointer" variant="outline" size="sm" mr={2} mb={0} \> 📷 add photo \</button> \</box> \<button colorscheme="blue" onclick={handlecreatepost} isloading={isposting} disabled={(!newpostcontent trim() && !selectedimage) || isposting} \> post \</button> \</box> {/ posts feed with images /} {isloading ? ( \<center py={10}> \<spinner size="xl" /> \</center> ) posts length > 0 ? ( \<vstack align="stretch" spacing={4}> {posts map(post => ( \<box key={post id} border="1px solid" bordercolor="gray 600" p={4} borderradius="md" \> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(post createdat)}\</text> \</hstack> \<text mb={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box mb={4}> \<image src={post image} alt="post image" borderradius="md" maxh="400px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" size="sm" onclick={() => handlelikepost(post id)} \> ❤️ {post likes} \</button> \<button variant="ghost" size="sm" as={routerlink} to={`/post/${post id}`} \> 💬 comment \</button> \<button variant="ghost" size="sm"> 🔄 share \</button> \</hstack> \</box> ))} \</vstack> ) ( \<text>no posts yet follow users or create your first post!\</text> )} \</box> {/ right sidebar (trending hashtags) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderleft="1px solid" bordercolor="gray 600" \> \<heading size="md" mb={4}> trending today \</heading> \<simplegrid columns={1} spacing={2}> \<button variant="outline" size="sm" colorscheme="whitealpha">#travel\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#tech\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#foodie\</button> \</simplegrid> \</box> \</flex> ); } export default feedpage; 这个 feed 组件包含几个关键特性: 身份验证检查 确保只有登录用户可以查看该 feed 帖子创建 允许用户创建带有文本和可选图像的新帖子 图像上传 处理图像选择、预览和使用 parse file 上传 feed 显示 以倒序时间顺序显示所有用户的帖子 点赞功能 允许用户使用简单的计数器方法点赞帖子 导航 提供链接到应用程序的其他部分 让我们看看 back4app 如何帮助我们实现这些功能: 使用 parse 创建帖子 back4app 使得创建带有图像的帖子变得简单: // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); } // save the post const savedpost = await post save(); parse file 自动处理文件上传、存储和 url 生成,使得将图像添加到帖子变得简单。 使用 parse 查询获取帖子 back4app的查询系统使获取和显示帖子变得简单: // create a query for the post class const query = new parse query('post'); // include the user who created the post query include('author'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query const results = await query find(); 这个 include('author') 方法特别强大,因为它会自动将每个帖子的引用用户对象包含在内,从而减少了多次查询的需要。 back4gram项目: 在 这里 找到完整的代码, 社交网络示例项目 ,使用back4app构建 步骤 4 – 实现带评论的帖子详细视图 现在,让我们创建一个帖子详细页面,显示单个帖子及其评论,并允许用户添加新评论。 创建一个名为 src/pages/postdetailspage js import react, { usestate, useeffect } from 'react'; import { useparams, usenavigate, link as routerlink } from 'react router dom'; import { box, flex, vstack, heading, text, button, avatar, hstack, textarea, spinner, center, image } from '@chakra ui/react'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function postdetailspage() { const { id } = useparams(); const navigate = usenavigate(); const \[post, setpost] = usestate(null); const \[comments, setcomments] = usestate(\[]); const \[newcomment, setnewcomment] = usestate(''); const \[isloading, setisloading] = usestate(true); const \[iscommenting, setiscommenting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // fetch post and comments useeffect(() => { const fetchpostdetails = async () => { if (!currentuser) return; setisloading(true); try { // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); // convert post to plain object const fetchedpost = { id postobject id, content postobject get('content'), author { id postobject get('author') id, username postobject get('author') get('username'), avatar postobject get('author') get('avatar') ? postobject get('author') get('avatar') url() null }, likes postobject get('likes') || 0, createdat postobject get('createdat'), image postobject get('image') ? postobject get('image') url() null }; // convert comments to plain objects const fetchedcomments = commentresults map(comment => ({ id comment id, content comment get('content'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null }, createdat comment get('createdat') })); setpost(fetchedpost); setcomments(fetchedcomments); } catch (error) { console error('error fetching post details ', error); toaster create({ title 'error loading post', description error message, type 'error', }); navigate('/feed'); } finally { setisloading(false); } }; if (id) { fetchpostdetails(); } }, \[id, currentuser, navigate]); // function to add a comment const handleaddcomment = async () => { if (!newcomment trim()) return; setiscommenting(true); try { // get the post object const postquery = new parse query('post'); const postobject = await postquery get(id); // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); // add the new comment to the state const newcommentobj = { id comment id, content comment get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, createdat comment get('createdat') }; setcomments(\[ comments, newcommentobj]); setnewcomment(''); toaster create({ title 'comment added', description 'your comment has been posted successfully!', type 'success', }); } catch (error) { console error('error adding comment ', error); toaster create({ title 'error adding comment', description error message, type 'error', }); } finally { setiscommenting(false); } }; // function to like the post const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // increment likes postobject increment('likes'); await postobject save(); // update the post in the state setpost({ post, likes post likes + 1 }); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; if (isloading) { return ( \<center h="100vh"> \<spinner size="xl" /> \</center> ); } if (!post) { return ( \<center h="100vh"> \<vstack> \<text>post not found\</text> \<button as={routerlink} to="/feed" colorscheme="blue"> back to feed \</button> \</vstack> \</center> ); } return ( \<box maxw="800px" mx="auto" p={4}> \<button as={routerlink} to="/feed" mb={4} variant="ghost"> ← back to feed \</button> {/ post /} \<box border="1px solid" bordercolor="gray 600" p={6} borderradius="md" mb={6}> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<vstack align="start" spacing={0}> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">{formatdate(post createdat)}\</text> \</vstack> \</hstack> \<text fontsize="lg" my={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box my={4}> \<image src={post image} alt="post image" borderradius="md" maxh="500px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" onclick={handlelikepost} \> ❤️ {post likes} \</button> \<button variant="ghost"> 🔄 share \</button> \</hstack> \</box> {/ comments section /} \<box> \<heading size="md" mb={4}> comments ({comments length}) \</heading> {/ add comment /} \<box mb={6}> \<textarea placeholder="write a comment " value={newcomment} onchange={(e) => setnewcomment(e target value)} mb={2} /> \<button colorscheme="blue" onclick={handleaddcomment} isloading={iscommenting} disabled={!newcomment trim()} \> post comment \</button> \</box> \<hr style={{ marginbottom "1rem" }} /> {/ comments list /} {comments length > 0 ? ( \<vstack align="stretch" spacing={4}> {comments map(comment => ( \<box key={comment id} p={4} bg="gray 700" borderradius="md"> \<hstack mb={2}> \<avatar src={comment author avatar} name={comment author username} size="sm" /> \<text fontweight="bold">{comment author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(comment createdat)}\</text> \</hstack> \<text>{comment content}\</text> \</box> ))} \</vstack> ) ( \<text color="gray 400">no comments yet be the first to comment!\</text> )} \</box> \</box> ); } export default postdetailspage; this post detail page provides several important features 1\ post display shows the complete post with author information, content, image, and likes 2\ comment creation allows users to add new comments to the post 3\ comment listing displays all comments in chronological order 4\ like functionality enables users to like the post let's examine the key aspects of how back4app is helping us implement these features \### fetching post details with related objects back4app makes it easy to fetch a post with related information ```javascript // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); 该 include('author') 方法自动检索与帖子相关的用户对象,消除了单独查询的需要。 查询相关评论 back4app 的查询系统允许我们找到与特定帖子相关的所有评论: // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); 该 equalto('post', postobject) 方法过滤评论,仅保留与当前帖子相关的评论。 使用指针创建评论 创建与帖子相关的评论非常简单: // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); 该 帖子 字段被设置为指向帖子对象,建立了评论与帖子的关系。 back4gram 项目: 查找 这里 完整代码,适用于 社交网络示例项目 ,使用 back4app 构建。 步骤 5 – 使用 parse 实现高级点赞功能 到目前为止,我们实现的简单点赞计数器在基本功能上运行良好,但它有一些局限性: 用户可以多次点赞一条帖子 用户不能“取消点赞”一条帖子 我们无法显示谁点赞了帖子 让我们创建一个更高级的点赞系统,使用一个单独的 点赞 类来跟踪单个点赞。 设置点赞类 首先,在您的 back4app 控制面板中创建一个新类: 在数据库浏览器中点击“创建一个类” 输入“like”作为类名,并选择“自定义”作为类型 添加以下列: 用户 (类型:指针,目标类: user) 帖子 (类型:指针,目标类:post) 在我们的代码中实现点赞管理 现在,让我们更新我们在 postdetailspage js 文件中的帖子点赞功能: // add this state at the beginning of the component const \[hasliked, sethasliked] = usestate(false); // add this to the useeffect that fetches post details // check if current user has liked this post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const userlike = await likequery first(); sethasliked(!!userlike); // replace the handlelikepost function with this new version const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // check if user has already liked the post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const existinglike = await likequery first(); if (existinglike) { // user already liked the post, so remove the like await existinglike destroy(); // decrement likes count on the post postobject increment('likes', 1); await postobject save(); // update ui sethasliked(false); setpost({ post, likes post likes 1 }); } else { // user hasn't liked the post yet, so add a like const like = parse object extend('like'); const like = new like(); like set('user', currentuser); like set('post', postobject); await like save(); // increment likes count on the post postobject increment('likes'); await postobject save(); // update ui sethasliked(true); setpost({ post, likes post likes + 1 }); } } catch (error) { console error('error toggling like ', error); toaster create({ title 'error', description 'could not process your like please try again ', type 'error', }); } }; 然后,更新点赞按钮以反映当前状态: \<button variant="ghost" onclick={handlelikepost} color={hasliked ? "red 500" "inherit"} \> {hasliked ? "❤️" "🤍"} {post likes} \</button> 这个实现: 使用单独的 点赞 类别跟踪单个点赞 允许用户切换点赞(点赞和取消点赞) 直观地指示当前用户是否已点赞该帖子 维护帖子上的点赞计数以便于查询 显示谁喜欢了一条帖子 您还可以添加功能以显示谁喜欢了一条帖子: // add this state const \[likeusers, setlikeusers] = usestate(\[]); const \[showlikes, setshowlikes] = usestate(false); // add this function const fetchlikes = async () => { try { const postquery = new parse query('post'); const postobject = await postquery get(id); const likequery = new parse query('like'); likequery equalto('post', postobject); likequery include('user'); const likes = await likequery find(); const users = likes map(like => { const user = like get('user'); return { id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null }; }); setlikeusers(users); setshowlikes(true); } catch (error) { console error('error fetching likes ', error); } }; 然后添加一个用户界面元素以显示喜欢该帖子的用户: \<text color="blue 400" cursor="pointer" onclick={fetchlikes} fontsize="sm" \> see who liked this post \</text> {showlikes && ( \<box mt={2} p={2} bg="gray 700" borderradius="md"> \<heading size="xs" mb={2}>liked by \</heading> {likeusers length > 0 ? ( \<vstack align="stretch"> {likeusers map(user => ( \<hstack key={user id}> \<avatar size="xs" src={user avatar} name={user username} /> \<text fontsize="sm">{user username}\</text> \</hstack> ))} \</vstack> ) ( \<text fontsize="sm">no likes yet\</text> )} \</box> )} back4gram 项目: 找到 这里 完整代码的 社交网络示例项目 使用 back4app 构建 第 6 步 – 使用 parse live query 实现实时更新 back4app 最强大的功能之一是能够通过 live query 接收实时更新。这使得您的应用程序在创建新帖子或评论时能够自动更新,而无需手动刷新。 在 back4app 上设置实时查询 首先,您需要在 back4app 中为您的应用程序启用实时查询: 转到您的 back4app 控制面板 导航到应用设置 > 服务器设置 向下滚动到 "parse live query class names" 将 "post" 和 "comment" 添加到类列表中 保存您的更改 为实时帖子实现实时查询 让我们更新我们的 feedpage js 以包含实时帖子的 live query 更新: // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser) return; try { console log('setting up live query for posts'); // create a query that will be used for the subscription const query = new parse query('post'); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new posts livequerysubscription current on('create', (post) => { console log('new post received via live query ', post id); // skip posts from the current user as they're already in the ui if (post get('author') id === currentuser id) { console log('skipping post from current user'); return; } // format the new post const author = post get('author'); const newpost = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; // add the new post to the posts state setposts(prevposts => \[newpost, prevposts]); }); // handle post updates livequerysubscription current on('update', (post) => { console log('post updated via live query ', post id); // format the updated post const newlikes = post get('likes') || 0; // update the post in the state setposts(prevposts => prevposts map(p => p id === post id ? { p, likes newlikes } p ) ); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser]); 实现实时评论的 live query 同样,让我们更新我们的 postdetailspage js 以包含实时评论的 live query: // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser || !id) return; try { console log('setting up live query for comments'); // get the post object to use in the query const postquery = new parse query('post'); const postobject = await postquery get(id); // create a query that will be used for the subscription const query = new parse query('comment'); query equalto('post', postobject); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query for comments'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new comments livequerysubscription current on('create', (comment) => { console log('new comment received via live query ', comment id); // skip comments from the current user as they're already in the ui if (comment get('author') id === currentuser id) { console log('skipping comment from current user'); return; } // format the new comment const author = comment get('author'); const newcomment = { id comment id, content comment get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, createdat comment get('createdat') }; // add the new comment to the comments state setcomments(prevcomments => \[ prevcomments, newcomment]); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query for comments ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser, id]); 通过这些实时查询实现,你的社交网络现在将实时更新: 其他用户的新帖子将自动出现在动态中 当其他用户点赞帖子时,点赞数将实时更新 新评论将自动出现在帖子详情页上 back4gram 项目: 找到 这里 完整代码的 社交网络示例项目 使用 back4app 构建 步骤 7 – 高级查询和性能优化 随着社交网络的增长,性能变得越来越重要。让我们探索一些高级技术,以利用 back4app 强大的查询能力来优化您的应用程序。 实现分页 与其一次性加载所有帖子,不如实现分页以批量加载它们: // add these state variables const \[page, setpage] = usestate(0); const \[hasmoreposts, sethasmoreposts] = usestate(true); const postsperpage = 10; // update your fetchposts function const fetchposts = async (loadmore = false) => { if (!currentuser) return; if (!loadmore) { setisloading(true); } try { const query = new parse query('post'); query include('author'); query descending('createdat'); query limit(postsperpage); query skip(page postsperpage); const results = await query find(); // process and format posts as before const fetchedposts = \[ ]; // update the hasmoreposts state sethasmoreposts(results length === postsperpage); // update the posts state if (loadmore) { setposts(prevposts => \[ prevposts, fetchedposts]); } else { setposts(fetchedposts); } // update the page if this was a "load more" request if (loadmore) { setpage(prevpage => prevpage + 1); } } catch (error) { // handle errors } finally { setisloading(false); } }; // add a load more button at the bottom of your posts list {hasmoreposts && ( \<button onclick={() => fetchposts(true)} variant="outline" isloading={isloading} mt={4} \> load more posts \</button> )} 实现用户特定的动态 为了获得更个性化的体验,你可能想要仅显示当前用户关注的用户的帖子: // assuming you have a "follow" class with "follower" and "following" pointers const fetchfollowingposts = async () => { try { // first, find all users that the current user follows const followquery = new parse query('follow'); followquery equalto('follower', currentuser); const followings = await followquery find(); // extract the users being followed const followedusers = followings map(follow => follow\ get('following')); // add the current user to show their posts too followedusers push(currentuser); // create a query for posts from these users const query = new parse query('post'); query containedin('author', followedusers); query include('author'); query descending('createdat'); query limit(20); const results = await query find(); // process results } catch (error) { // handle errors } }; 使用云函数进行复杂操作 对于需要多个查询的复杂操作,请考虑使用 back4app 云函数: 前往您的 back4app 控制面板 导航到云代码 > 云函数 创建一个新函数: // example function to get a feed with posts, likes, and comment counts parse cloud define("getfeed", async (request) => { try { const user = request user; const page = request params page || 0; const limit = request params limit || 10; if (!user) { throw new error("user must be logged in"); } // query for posts const query = new parse query("post"); query include("author"); query descending("createdat"); query limit(limit); query skip(page limit); const posts = await query find({ usemasterkey true }); // format posts and get additional info const formattedposts = await promise all(posts map(async (post) => { // check if user has liked this post const likequery = new parse query("like"); likequery equalto("post", post); likequery equalto("user", user); const hasliked = await likequery count({ usemasterkey true }) > 0; // get comment count const commentquery = new parse query("comment"); commentquery equalto("post", post); const commentcount = await commentquery count({ usemasterkey true }); // format post for response return { id post id, content post get("content"), image post get("image") ? post get("image") url() null, author { id post get("author") id, username post get("author") get("username"), avatar post get("author") get("avatar") ? post get("author") get("avatar") url() null }, likes post get("likes") || 0, hasliked hasliked, commentcount commentcount, createdat post get("createdat") }; })); return { posts formattedposts }; } catch (error) { throw new error(`error fetching feed ${error message}`); } }); 然后从您的应用程序调用此云函数: const fetchfeed = async () => { try { setisloading(true); const result = await parse cloud run('getfeed', { page 0, limit 10 }); setposts(result posts); } catch (error) { console error('error fetching feed ', error); toaster create({ title 'error loading feed', description error message, type 'error', }); } finally { setisloading(false); } }; 这种方法可以显著减少对数据库的查询次数并提高性能。 back4gram 项目: 找到 这里 完整代码的 社交网络示例项目 使用 back4app 构建 结论 在本教程中,您使用 back4app 构建了一个全面的社交网络动态,具有互动功能。让我们回顾一下您所完成的工作: 数据模型设计 创建了一个结构化的数据模型,用于帖子、评论和点赞。 动态实现 构建了一个动态页面,显示所有用户的帖子。 帖子创建 实现了用户创建带有文本和图像的帖子的功能。 评论系统 创建了一个评论系统,允许用户与帖子互动。 点赞功能 实现了简单和高级的点赞系统。 实时更新 添加了实时查询以获取帖子和评论的实时更新。 性能优化 探索了在应用程序扩展时提高性能的技术。 back4app 提供了: 一个安全且可扩展的数据库,用于存储用户生成的内容 用于帖子图片的文件存储 内置用户身份验证以确保安全 实时查询以获取实时更新 云函数用于复杂的后端操作 有了这些构建块,您的社交网络有了一个坚实的基础,可以随着用户基础的增长而扩展。 下一步 为了进一步增强您的社交网络,考虑实施以下功能: 通知 当用户的帖子收到点赞或评论时提醒用户。 用户关注系统 允许用户相互关注并自定义他们的动态。 标签和搜索 实施标签系统和增强的搜索功能。 丰富媒体帖子 支持视频、多张图片和其他媒体类型。 分析 跟踪参与度和用户行为,以改善您的应用程序。 要查看back4gram社交网络应用程序的完整代码,您可以查看 github仓库 https //github com/templates back4app/back4gram 。 通过利用back4app强大的后端服务,您可以专注于创造出色的用户体验,而平台则处理社交网络应用程序所需的复杂基础设施。