Project Templates
Social Network
あなたのソーシャルネットワークのフィードとインタラクション
36 分
イントロダクション このチュートリアルでは、back4appをバックエンドサービスとして使用して、投稿、コメント、いいねなどのインタラクティブな機能を持つソーシャルネットワークフィードを構築する方法を学びます。これらの機能は、ユーザーがコンテンツを共有し、他のユーザーと交流できるようにする、あらゆるソーシャルネットワーキングアプリケーションの核心を形成します。 back4appは、parse server上に構築されたbackend as a service(baas)プラットフォームで、開発者にサーバーサイドコードを管理することなくスケーラブルなアプリケーションを作成するための堅牢なインフラストラクチャを提供します。リアルタイムデータベース機能と組み込みのユーザー管理により、ソーシャルネットワーキングアプリケーションの開発に最適な選択肢となります。 このチュートリアルの終わりまでに、ユーザーが以下のことを行える完全に機能するソーシャルフィードを実装することができます: テキストと画像を含む投稿を作成する フィード内の他のユーザーの投稿を表示する 投稿にいいねをする 投稿にコメントする コメント付きの投稿詳細を表示する データベースクラスを効率的に構造化し、リアルタイム更新を実装し、デバイス間でシームレスな体験を提供するレスポンシブなユーザーインターフェースを作成する方法を学びます。 前提条件 このチュートリアルを完了するには、以下が必要です: back4appアカウント。 back4app com https //www back4app com back4appプロジェクトのセットアップ。 back4appの使い方ガイド https //www back4app com/docs/get started/welcome ローカルマシンにnode jsがインストールされていること。 javascriptとreact jsの基本的な知識。 動作する認証システム。まだ設定していない場合は、まず私たちの ソーシャルネットワークのための認証システム チュートリアルに従ってください。 モダンなウェブ開発の概念(コンポーネント、状態管理など)に慣れていること。 back4gramプロジェクト: 完全なコードを見つけるには、 こちら で、 ソーシャルネットワークのサンプルプロジェクト を見つけてください。 ステップ1 – データモデルの理解 コードを書く前に、私たちのソーシャルネットワークのデータモデルを理解することが重要です。フィード機能を構築するために、いくつかのparseクラスが必要になります: 投稿クラス この 投稿 クラスは、すべての投稿関連情報を保存します: 著者 投稿を作成したユーザーへのポインタ コンテンツ 投稿のテキストコンテンツ 画像 オプションの画像ファイル いいね 投稿が受けたいいねの数を表す数値 コメント コメントオブジェクトへのポインタの配列(オプション、別にクエリ可能) 作成日時 back4appによって自動的に管理されます 更新日時 back4appによって自動的に管理されます コメントクラス この コメント クラスは投稿に対するコメントを保存します: 著者 コメントを書いたユーザーへのポインタ 投稿 コメントされている投稿へのポインタ 内容 コメントのテキスト内容 作成日時 back4appによって自動的に管理されます 更新日時 back4appによって自動的に管理されます いいねの追跡 いいねを追跡するための2つのオプションがあります: シンプルカウンター 各投稿にいいねのカウントを保存します(簡単ですが詳細は少ない) いいねの記録 各個別のいいねを追跡する別の いいね クラスを作成します(より詳細) このチュートリアルでは、まずシンプルなカウンターを実装し、その後、より高度な機能のために個別のいいねの記録を実装する方法を示します。 これらのクラスをback4appで設定することから始めましょう。 ステップ 2 – back4app でデータモデルを設定する このステップでは、back4app データベースに必要なクラスを作成します。 ポストクラスの作成 back4app ダッシュボードにログインし、プロジェクトに移動します。 左のサイドバーで「データベース」をクリックして、データベースブラウザを開きます。 ページの上部にある「クラスを作成」ボタンをクリックします。 表示されるモーダルで、「post」をクラス名として入力し、「カスタム」をタイプとして選択します。次に「クラスを作成」をクリックします。 \[image back4app クラス作成モーダルで「post」と入力されたクラス名] 次に、postクラスに以下のカラムを追加します 「カラムを追加」をクリック 以下のカラムを作成します 著者 (タイプ ポインタ, ターゲットクラス user) コンテンツ (タイプ 文字列) 画像 (タイプ ファイル) いいね (タイプ 数字, デフォルト値 0) コメントクラスの作成 「クラスを作成」を再度クリックします。 クラス名に「comment」と入力し、タイプに「カスタム」を選択します。 以下のカラムを追加します 著者 (タイプ ポインタ, ターゲットクラス user) 投稿 (タイプ ポインタ, ターゲットクラス post) コンテンツ (タイプ 文字列) クラスレベルの権限の設定 セキュリティのために、クラスに適切な権限を設定しましょう: データベースブラウザで、postクラスを選択します。 「セキュリティ」ボタンをクリックします。 権限を設定します: 公開読み取り はい(誰でも投稿を閲覧できます) 公開書き込み いいえ(認証されたユーザーのみが投稿を作成できます) 公開フィールド追加 いいえ 検索、取得、作成、更新、削除 ユーザー認証が必要 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; このフィードコンポーネントにはいくつかの重要な機能が含まれています: 認証チェック ログインしているユーザーのみがフィードを表示できることを保証します 投稿作成 ユーザーがテキストとオプションの画像を使って新しい投稿を作成できるようにします 画像アップロード 画像の選択、プレビュー、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クエリを使用した投稿の取得 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プロジェクト: を見つける こちら 完全なコードは、 ソーシャルネットワークのサンプルプロジェクト を使用して構築されています。 ステップ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」を入力し、タイプに「custom」を選択します 次の列を追加します: ユーザー (タイプ:ポインタ、ターゲットクラス: 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); } }; 次に、投稿をいいねしたユーザーを表示する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の最も強力な機能の1つは、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 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プロジェクト: を見つける こちら の完全なコードは、 ソーシャルネットワークのサンプルプロジェクト を使用して構築されています。 結論 このチュートリアルでは、back4appを使用してインタラクティブな機能を持つ包括的なソーシャルネットワークフィードを構築しました。あなたが達成したことを振り返りましょう: データモデル設計 投稿、コメント、いいねのための構造化されたデータモデルを作成しました。 フィード実装 すべてのユーザーからの投稿を表示するフィードページを構築しました。 投稿作成 ユーザーがテキストと画像を使って投稿を作成できる機能を実装しました。 コメントシステム ユーザーが投稿に対して関与できるコメントシステムを作成しました。 いいね機能 シンプルないいねシステムと高度ないいねシステムの両方を実装しました。 リアルタイム更新 リアルタイムの投稿とコメントの更新のためにライブクエリを追加しました。 パフォーマンス最適化 アプリがスケールするにつれてパフォーマンスを向上させる技術を探求しました。 back4appが提供したもの: ユーザー生成コンテンツを保存するための安全でスケーラブルなデータベース 投稿画像のファイルストレージ セキュリティのための組み込みユーザー認証 リアルタイム更新のためのライブクエリ 複雑なバックエンド操作のためのクラウドファンクション これらのビルディングブロックが整ったことで、あなたのソーシャルネットワークはユーザーベースの成長に合わせてスケールできる堅固な基盤を持っています。 次のステップ ソーシャルネットワークをさらに強化するために、これらの機能の実装を検討してください: 通知 投稿が「いいね」やコメントを受け取ったときにユーザーにアラートを送信します。 ユーザーフォローシステム ユーザー同士がフォローし合い、フィードをカスタマイズできるようにします。 ハッシュタグと検索 ハッシュタグシステムと強化された検索機能を実装します。 メディアリッチな投稿 動画、複数の画像、その他のメディアタイプをサポートします。 分析 エンゲージメントとユーザー行動を追跡してアプリケーションを改善します。 back4gramソーシャルネットワークアプリケーションの完全なコードは、以下をチェックしてください。 githubリポジトリ https //github com/templates back4app/back4gram 。 back4appの強力なバックエンドサービスを活用することで、プラットフォームがソーシャルネットワーキングアプリケーションに必要な複雑なインフラを処理している間、素晴らしいユーザー体験の創造に集中できます。