Project Templates
Social Network
ฟีดและการโต้ตอบสำหรับเครือข่ายสังคมของคุณ
36 นาที
บทนำ ในบทเรียนนี้ คุณจะได้เรียนรู้วิธีการสร้างฟีดโซเชียลเน็ตเวิร์กที่มีฟีเจอร์เชิงโต้ตอบ เช่น โพสต์ ความคิดเห็น และการกดถูกใจ โดยใช้ back4app เป็นบริการแบ็กเอนด์ของคุณ ฟีเจอร์เหล่านี้เป็นแกนหลักของแอปพลิเคชันโซเชียลเน็ตเวิร์กใด ๆ ช่วยให้ผู้ใช้สามารถแชร์เนื้อหาและมีส่วนร่วมกับผู้ใช้อื่น ๆ ได้ back4app เป็นแพลตฟอร์ม backend as a service (baas) ที่สร้างขึ้นบน parse server ซึ่งให้โครงสร้างพื้นฐานที่แข็งแกร่งแก่ผู้พัฒนาในการสร้างแอปพลิเคชันที่สามารถขยายได้โดยไม่ต้องจัดการกับโค้ดฝั่งเซิร์ฟเวอร์ ความสามารถของฐานข้อมูลเรียลไทม์และการจัดการผู้ใช้ในตัวทำให้มันเป็นตัวเลือกที่ยอดเยี่ยมสำหรับการพัฒนาแอปพลิเคชันโซเชียลเน็ตเวิร์ก เมื่อสิ้นสุดบทเรียนนี้ คุณจะได้สร้างฟีดโซเชียลที่ใช้งานได้อย่างสมบูรณ์ซึ่งอนุญาตให้ผู้ใช้ สร้างโพสต์ด้วยข้อความและภาพ ดูโพสต์จากผู้ใช้อื่นในฟีด กดถูกใจโพสต์ แสดงความคิดเห็นในโพสต์ ดูรายละเอียดโพสต์พร้อมความคิดเห็น คุณจะได้เรียนรู้วิธีการจัดโครงสร้างคลาสฐานข้อมูลของคุณอย่างมีประสิทธิภาพ ใช้การอัปเดตเรียลไทม์ และสร้างส่วนติดต่อผู้ใช้ที่ตอบสนองซึ่งมอบประสบการณ์ที่ราบรื่นข้ามอุปกรณ์ต่าง ๆ ข้อกำหนดเบื้องต้น เพื่อให้เสร็จสิ้นบทเรียนนี้ คุณจะต้องมี บัญชี 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 ของคุณและไปที่โปรเจกต์ของคุณ ในแถบด้านซ้าย คลิกที่ "ฐานข้อมูล" เพื่อเปิดเบราว์เซอร์ฐานข้อมูล คลิกที่ปุ่ม "สร้างคลาส" ที่ด้านบนของหน้า ในโมดัลที่ปรากฏขึ้น ให้ป้อน "โพสต์" เป็นชื่อคลาสและเลือก "กำหนดเอง" เป็นประเภท จากนั้นคลิก "สร้างคลาส" \[image back4app สร้างคลาส modal โดยมี "post" ป้อนเป็นชื่อคลาส] ตอนนี้ เพิ่มคอลัมน์ต่อไปนี้ในคลาส post ของคุณ คลิก "เพิ่มคอลัมน์" สร้างคอลัมน์ต่อไปนี้ ผู้เขียน (ประเภท pointer, คลาสเป้าหมาย user) เนื้อหา (ประเภท string) ภาพ (ประเภท file) ไลค์ (ประเภท number, ค่าเริ่มต้น 0) การสร้างคลาสความคิดเห็น คลิก "สร้างคลาส" อีกครั้ง ป้อน "ความคิดเห็น" เป็นชื่อคลาสและเลือก "กำหนดเอง" เป็นประเภท เพิ่มคอลัมน์ต่อไปนี้ ผู้เขียน (ประเภท pointer, คลาสเป้าหมาย user) โพสต์ (ประเภท pointer, คลาสเป้าหมาย post) เนื้อหา (ประเภท string) การตั้งค่าการอนุญาตระดับคลาส เพื่อความปลอดภัย มาตั้งค่าการอนุญาตที่เหมาะสมสำหรับคลาสของเรากันเถอะ ใน database browser ให้เลือกคลาส post คลิกที่ปุ่ม "security" กำหนดการอนุญาต อ่านสาธารณะ ใช่ (ใครก็ได้สามารถดูโพสต์ได้) เขียนสาธารณะ ไม่ (เฉพาะผู้ใช้ที่ผ่านการตรวจสอบเท่านั้นที่สามารถสร้างโพสต์ได้) เพิ่มฟิลด์สาธารณะ ไม่ ค้นหา, รับ, สร้าง, อัปเดต, ลบ ต้องการการตรวจสอบผู้ใช้ ทำการตั้งค่าแบบเดียวกันสำหรับคลาส comment ตอนนี้เรามีการตั้งค่ารูปแบบข้อมูลแล้ว มาติดตั้งส่วนประกอบด้านหน้าเพื่อฟีดโซเชียลเน็ตเวิร์กของเรากันเถอะ หน้าแสดงฟีดเป็นหัวใจของเครือข่ายสังคมใดๆ มันแสดงโพสต์จากผู้ใช้และอนุญาตให้มีการโต้ตอบ มาสร้างส่วนประกอบฟีดที่ดึงข้อมูลและแสดงโพสต์กันเถอะ สร้างไฟล์ชื่อ 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 นี้รวมคุณสมบัติหลักหลายอย่างไว้ด้วยกัน การตรวจสอบการเข้าสู่ระบบ รับประกันว่าผู้ใช้ที่เข้าสู่ระบบเท่านั้นที่สามารถดูฟีดได้ การสร้างโพสต์ อนุญาตให้ผู้ใช้สร้างโพสต์ใหม่ด้วยข้อความและภาพที่เลือกได้ การอัปโหลดภาพ จัดการการเลือกภาพ การแสดงตัวอย่าง และการอัปโหลดโดยใช้ parse file การแสดงฟีด แสดงโพสต์จากผู้ใช้ทั้งหมดในลำดับย้อนกลับตามเวลา ฟังก์ชันการถูกใจ อนุญาตให้ผู้ใช้ถูกใจโพสต์โดยใช้วิธีการนับที่ง่าย การนำทาง ให้ลิงก์ไปยังส่วนอื่น ๆ ของแอปพลิเคชัน มาดูว่า 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 query ระบบการค้นหาของ 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 ตัวนับถูกใจที่เรานำไปใช้จนถึงตอนนี้ทำงานได้ดีสำหรับฟังก์ชันพื้นฐาน แต่มีข้อจำกัด ผู้ใช้สามารถกดถูกใจโพสต์ได้หลายครั้ง ผู้ใช้ไม่สามารถ "ยกเลิกการกดถูกใจ" โพสต์ได้ เราไม่สามารถแสดงได้ว่าใครกดถูกใจโพสต์ มาสร้างระบบกดถูกใจที่ซับซ้อนมากขึ้นโดยใช้ คลาส like เพื่อติดตามการกดถูกใจแต่ละรายการ การตั้งค่าคลาส like ก่อนอื่น สร้างคลาสใหม่ในแดชบอร์ด back4app ของคุณ คลิก "สร้างคลาส" ใน database browser ใส่ "like" เป็นชื่อคลาสและเลือก "custom" เป็นประเภท เพิ่มคอลัมน์ต่อไปนี้ ผู้ใช้ (ประเภท pointer, คลาสเป้าหมาย user) โพสต์ (ประเภท pointer, คลาสเป้าหมาย 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> การนำไปใช้นี้ ติดตามการถูกใจแต่ละรายการโดยใช้ like คลาส อนุญาตให้ผู้ใช้สลับการถูกใจ (ถูกใจและไม่ถูกใจ) แสดงให้เห็นว่าผู้ใช้ปัจจุบันได้ถูกใจโพสต์หรือไม่ รักษาจำนวนการถูกใจในโพสต์เพื่อการค้นหาที่มีประสิทธิภาพ แสดงผู้ที่กดถูกใจโพสต์ คุณยังสามารถเพิ่มฟังก์ชันการทำงานเพื่อแสดงว่าผู้ใดได้กดถูกใจโพสต์ // 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); } }; จากนั้นเพิ่มองค์ประกอบ ui เพื่อแสดงผู้ใช้ที่กดถูกใจโพสต์ \<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 ซึ่งช่วยให้แอปพลิเคชันของคุณอัปเดตโดยอัตโนมัติเมื่อมีการสร้างโพสต์หรือความคิดเห็นใหม่โดยไม่ต้องรีเฟรชด้วยตนเอง การตั้งค่า live query บน back4app ก่อนอื่นคุณต้องเปิดใช้งาน live query สำหรับแอปพลิเคชันของคุณใน back4app ไปที่แดชบอร์ด back4app ของคุณ ไปที่การตั้งค่าแอป > การตั้งค่าเซิร์ฟเวอร์ เลื่อนลงไปที่ "parse live query class names" เพิ่ม "post" และ "comment" ลงในรายการคลาส บันทึกการเปลี่ยนแปลงของคุณ การใช้งาน live query สำหรับโพสต์แบบเรียลไทม์ มาปรับปรุง 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]); ด้วยการใช้งาน live query เหล่านี้ เครือข่ายสังคมของคุณจะอัปเดตแบบเรียลไทม์แล้ว โพสต์ใหม่จากผู้ใช้คนอื่นจะปรากฏโดยอัตโนมัติในฟีด จำนวนไลค์จะอัปเดตแบบเรียลไทม์เมื่อผู้ใช้อื่นกดไลค์โพสต์ ความคิดเห็นใหม่จะปรากฏโดยอัตโนมัติในหน้ารายละเอียดโพสต์ back4gram project ค้นหา ที่นี่ โค้ดทั้งหมดสำหรับ ตัวอย่างโปรเจกต์เครือข่ายสังคม ที่สร้างด้วย 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 } }; การใช้ cloud functions สำหรับการดำเนินการที่ซับซ้อน สำหรับการดำเนินการที่ซับซ้อนซึ่งต้องการการค้นหาหลายครั้ง ให้พิจารณาใช้ back4app cloud functions ไปที่แดชบอร์ด back4app ของคุณ ไปที่ cloud code > cloud functions สร้างฟังก์ชันใหม่ // 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}`); } }); จากนั้นเรียกใช้ cloud function นี้จากแอปของคุณ 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 project ค้นหา ที่นี่ โค้ดทั้งหมดสำหรับ โปรเจกต์ตัวอย่างเครือข่ายสังคม ที่สร้างขึ้นด้วย back4app บทสรุป ในบทเรียนนี้ คุณได้สร้างฟีดโซเชียลเน็ตเวิร์กที่ครอบคลุมพร้อมฟีเจอร์เชิงโต้ตอบโดยใช้ back4app มาสรุปสิ่งที่คุณได้ทำสำเร็จ การออกแบบโมเดลข้อมูล สร้างโมเดลข้อมูลที่มีโครงสร้างสำหรับโพสต์ ความคิดเห็น และการกดถูกใจ การนำฟีดไปใช้ สร้างหน้าแสดงฟีดที่แสดงโพสต์จากผู้ใช้ทั้งหมด การสร้างโพสต์ นำฟังก์ชันการทำงานสำหรับผู้ใช้ในการสร้างโพสต์ด้วยข้อความและภาพ ระบบความคิดเห็น สร้างระบบความคิดเห็นที่อนุญาตให้ผู้ใช้มีส่วนร่วมกับโพสต์ ฟังก์ชันการกดถูกใจ นำระบบการกดถูกใจทั้งแบบง่ายและแบบขั้นสูงมาใช้ การอัปเดตแบบเรียลไทม์ เพิ่ม live query สำหรับการอัปเดตโพสต์และความคิดเห็นแบบเรียลไทม์ การปรับปรุงประสิทธิภาพ สำรวจเทคนิคในการปรับปรุงประสิทธิภาพเมื่อแอปของคุณขยายตัว back4app ได้จัดเตรียม ฐานข้อมูลที่ปลอดภัยและสามารถขยายได้สำหรับการจัดเก็บเนื้อหาที่ผู้ใช้สร้างขึ้น การจัดเก็บไฟล์สำหรับภาพโพสต์ การตรวจสอบผู้ใช้ในตัวเพื่อความปลอดภัย การค้นหาสดสำหรับการอัปเดตแบบเรียลไทม์ ฟังก์ชันคลาวด์สำหรับการดำเนินการด้านหลังที่ซับซ้อน ด้วยบล็อกการสร้างเหล่านี้ในสถานที่ เครือข่ายสังคมของคุณมีพื้นฐานที่มั่นคงซึ่งสามารถขยายได้เมื่อฐานผู้ใช้ของคุณเติบโตขึ้น ขั้นตอนถัดไป เพื่อเพิ่มประสิทธิภาพเครือข่ายสังคมของคุณให้ดียิ่งขึ้น ให้พิจารณาการนำฟีเจอร์เหล่านี้ไปใช้ การแจ้งเตือน แจ้งเตือนผู้ใช้เมื่อโพสต์ของพวกเขาได้รับการถูกใจหรือความคิดเห็น ระบบติดตามผู้ใช้ อนุญาตให้ผู้ใช้ติดตามกันและปรับแต่งฟีดของพวกเขา แฮชแท็กและการค้นหา นำระบบแฮชแท็กและฟังก์ชันการค้นหาที่พัฒนาขึ้นมาใช้ โพสต์ที่มีสื่อหลากหลาย รองรับวิดีโอ ภาพหลายภาพ และประเภทสื่ออื่นๆ การวิเคราะห์ ติดตามการมีส่วนร่วมและพฤติกรรมของผู้ใช้เพื่อปรับปรุงแอปพลิเคชันของคุณ สำหรับรหัสทั้งหมดของแอปพลิเคชันเครือข่ายสังคม back4gram คุณสามารถตรวจสอบได้ที่ ที่เก็บ github https //github com/templates back4app/back4gram โดยการใช้บริการด้านหลังที่ทรงพลังของ back4app คุณสามารถมุ่งเน้นไปที่การสร้างประสบการณ์ผู้ใช้ที่ยอดเยี่ยมในขณะที่แพลตฟอร์มจัดการโครงสร้างพื้นฐานที่ซับซ้อนที่จำเป็นสำหรับแอปพลิเคชันเครือข่ายสังคม