Project Templates
Social Network
How to build a backend for a social network?
47 分
はじめに このガイドでは、ソーシャルネットワークのための堅牢なバックエンドを設定する方法を発見します。 バックエンド・アズ・ア・サービスプラットフォーム このバックエンドは、ユーザー認証、データ管理、リアルタイム通信などを処理するように設計されており、セキュリティと効率を確保します。 このチュートリアルの終わりには、あなたのプロジェクトは次のようになります ソーシャルネットワークの概要 ヒント 完全なソースコードは、私たちのgithubリポジトリを訪れてください github https //github com/templates back4app/back4gram 重要なポイント 簡素化されたバックエンド管理: back4appの管理されたバックエンドを活用して、ユーザー認証、データストレージ、リアルタイム機能を効率化します。 リアルタイムインタラクション: livequeryを利用して、動的なメッセージングと即時のコンテンツ更新を可能にします。 包括的なユーザーおよびコンテンツ管理: ユーザープロフィール、投稿、コメント、ソーシャルインタラクションのための堅牢なシステムを確立します。 スケーラビリティとパフォーマンス: クエリの最適化、キャッシング、バッチ処理を適用して、ネットワークが成長するにつれて高いパフォーマンスを維持します。 高度な統合: cloud functions、バックグラウンドジョブ、プッシュ通知を使用して、より豊かなユーザー体験のためにバックエンドを強化します。 前提条件 始める前に、次のものを確認してください: back4appアカウント 無料で登録するには back4app com https //www back4app com/ node jsとnpm node js (v14 x以上)を nodejs org https //nodejs org/ javascriptとreactの基本知識 コードエディタ visual studio codeやsublime textなどのエディタを使用します。 ステップ1 — back4appバックエンドの設定 back4appでの新しいプロジェクトの作成 back4appのダッシュボードにサインインします。 「 新しいアプリを作成 」をクリックします。 アプリに名前を付けます(例えば、「back4social」)そして、最寄りのサーバー地域を選択します。 プロジェクトが作成されると、back4appのダッシュボードにリストされます。このプロジェクトは、このチュートリアルで説明するすべてのバックエンド設定の基盤となります。 データベーススキーマの設計 あなたのソーシャルネットワークのバックエンドは、以下のクラスを使用します ユーザー (デフォルトでparseによって提供されます) バイオやプロフィール写真のようなフィールドで拡張する 投稿 テキストコンテンツと画像アップロードを保持する コメント 投稿に対するユーザーのコメントを保存する 会話 ユーザー間のチャットセッションを表す メッセージ 会話内の個々のメッセージを含む タイピングステータス ユーザーが入力中であることを示す データベースクラスの追加 back4appでは、データは クラス に保存されます。back4appダッシュボードまたはaiエージェントを介して新しいクラスを作成できます。 ダッシュボードを使用して作成するには、次の手順を実行してください 「データベース」セクションに移動 あなたのback4appダッシュボードで。 新しいクラスを作成 し、タイトル(string)やiscompleted(boolean)などの関連する列を追加します。 あなたのソーシャルネットワークのバックエンドは、次のクラスを使用します データベースを設定するには 「 "database" 」セクションに移動します。 ユーザークラスを強化する 既存の ユーザー クラスを開きます。 列を追加 バイオ (文字列) アバター (ファイル) フォロワー (数、デフォルト 0) 次の (番号、デフォルト 0) ポストクラスを作成する クリック "クラスを作成" 。 名前を付けて 投稿 を追加します コンテンツ (文字列) 著者 (ユーザーへのポインタ user ) 画像 (ファイル) いいね (数値、デフォルト 0) いいねした人 (配列) コメントクラスを作成する 名前を付けて コメント を含めて コンテンツ (文字列) 著者 (ユーザーへのポインタ user ) 投稿 (投稿へのポインタ) 会話クラスを作成する 名前を付けて 会話 を追加します 参加者 (配列) 最後のメッセージ (文字列) メッセージクラスを作成する 名前を付けて メッセージ を追加します テキスト (文字列) 送信者 (ポインタ先 ユーザー ) 会話 (会話へのポインタ) typingstatusクラスを作成する 名前を付けて typingstatus を追加します ユーザー (ポインタ先 ユーザー ) 会話 (会話へのポインタ) 入力中 (ブール値) aiエージェントを使用して作成するには、次の手順を実行してください aiエージェントを開く アプリダッシュボードまたはメニューから。 データモデルを説明する 簡単な言葉で aiエージェントにスキーマを作成させる 自動的に。 クラスの権限を設定する データを保護するために、クラスレベルの権限(clp)を調整します ダッシュボードの "セキュリティとキー" に移動します。 "クラスレベルのセキュリティ" の下で、読み取り/書き込みルールを設定します。 例えば、 post に対して公開の読み取りアクセスを許可し、更新/削除操作を投稿の著者に制限します。 リアルタイム機能をlivequeryで有効にする 「 サーバー設定 」に移動し、 parse server を見つけて、 livequery を有効にします。 リアルタイム監視のために、これらのクラスを含めます メッセージ タイピングステータス 投稿 (リアルタイムでいいねやコメントを更新するため) livequeryの設定 アプリケーションキーを取得します 「 アプリ設定 > 「 セキュリティとキー 」に移動します。 あなたの アプリケーションid , javascriptキー , サーバーurl , および livequeryサーバーurl を保存します。 セキュリティキー ステップ2 — フロントエンドをback4appにリンクする 環境変数の設定 プロジェクトのルートディレクトリに env local ファイルを作成します react app parse app id=your application id react app parse js key=your javascript key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your app id back4app io プレースホルダーを実際の資格情報に置き換えます。 parse sdkの設定 parseを初期化するための設定ファイルを作成します // src/utils/parseconfig js import parse from 'parse/dist/parse min js'; // initialize parse with your environment variables parse initialize( process env react app parse app id, process env react app parse js key ); parse serverurl = process env react app parse server url; // enable livequery if configured if (process env react app parse live query url) { parse livequeryserverurl = process env react app parse live query url; } export default parse; 次に、このファイルをアプリのエントリーポイント(例 index js ) にインポートします // src/index js import react from 'react'; import reactdom from 'react dom/client'; import ' /index css'; import app from ' /app'; import ' /utils/parseconfig'; // initialize parse const root = reactdom createroot(document getelementbyid('root')); root render( \<react strictmode> \<app /> \</react strictmode> ); ステップ 3 — ユーザー認証の設定 back4appのparse serverは、 parse user クラスを介して組み込みのユーザー管理システムを提供します。 parseがユーザーを管理する方法 ユーザーの資格情報(ユーザー名、メール、パスワード)を管理します 認証状態とセッショントークンを管理します 登録とログインのプロセスを簡素化します ユーザー登録の例 // function to handle user sign up const handlesignup = async () => { try { const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); // set additional user info user set('bio', ''); user set('followers', 0); user set('following', 0); await user signup(); console log('registration successful'); navigate('/feed'); } catch (error) { console error('sign up error ', error message); // handle errors based on error codes if (error code === 202) { seterrors({ errors, username 'username is taken'}); } else if (error code === 203) { seterrors({ errors, email 'email already exists'}); } } }; ユーザーログインの例 // function to handle user login const handlelogin = async () => { try { const user = await parse user login(username, password); console log('login successful ', user getusername()); navigate('/feed'); } catch (error) { console error('login error ', error message); setloginerror(error message); } }; 既存のセッションを確認中 const checkcurrentuser = async () => { try { const currentuser = parse user current(); if (currentuser) { console log('logged in as ', currentuser getusername()); return currentuser; } return null; } catch (error) { console error('error checking user ', error); return null; } }; パスワードリセットの実装 const handlepasswordreset = async () => { try { await parse user requestpasswordreset(email); console log('reset email sent'); setresetemailsent(true); } catch (error) { console error('password reset error ', error message); setreseterror(error message); } }; ステップ 4 — 投稿の処理 新しい投稿の作成 const createpost = async () => { if (!postcontent trim() && !postimage) { console error('please add content or an image'); return; } try { const post = parse object extend('post'); const newpost = new post(); newpost set('content', postcontent); newpost set('author', parse user current()); newpost set('likes', 0); newpost set('likedby', \[]); if (postimage) { const parsefile = new parse file(postimage name, postimage); await parsefile save(); newpost set('image', parsefile); } await newpost save(); console log('post created successfully'); return newpost; } catch (error) { console error('error creating post ', error message); throw error; } }; ステップ 4 — 投稿の処理 投稿の取得 const fetchposts = async (page = 0, limit = 10) => { try { const post = parse object extend('post'); const query = new parse query(post); query include('author'); query descending('createdat'); query limit(limit); query skip(page limit); const results = await query find(); const posts = results map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, likedby post get('likedby') || \[], createdat post get('createdat'), 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 } })); return posts; } catch (error) { console error('error fetching posts ', error); throw error; } }; いいね/いいね解除機能の実装 const togglelike = async (postid) => { try { const currentuser = parse user current(); const userid = currentuser id; const post = parse object extend('post'); const query = new parse query(post); const post = await query get(postid); const likedby = post get('likedby') || \[]; const isliked = likedby includes(userid); if (isliked) { post set('likedby', likedby filter(id => id !== userid)); post set('likes', math max((post get('likes') || 1) 1, 0)); } else { post set('likedby', \[ likedby, userid]); post set('likes', (post get('likes') || 0) + 1); } await post save(); return !isliked; } catch (error) { console error('error toggling like ', error); throw error; } }; ステップ5 — 投稿へのコメントの有効化 コメントの追加 const addcomment = async (postid, commentcontent) => { if (!commentcontent trim()) { console error('comment cannot be empty'); return; } try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const comment = new comment(); comment set('content', commentcontent); comment set('author', parse user current()); comment set('post', post); await comment save(); console log('comment added successfully'); return comment; } catch (error) { console error('error adding comment ', error); throw error; } }; 特定の投稿のコメントの取得 const fetchcomments = async (postid) => { try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const query = new parse query(comment); query equalto('post', post); query include('author'); query ascending('createdat'); const results = await query find(); const comments = results map(comment => ({ id comment id, content comment get('content'), createdat comment get('createdat'), 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 } })); return comments; } catch (error) { console error('error fetching comments ', error); throw error; } }; ステップ 6 — ユーザープロフィールの管理 ユーザープロフィールの取得 const fetchuserprofile = async (userid) => { try { const query = new parse query(parse user); const user = await query get(userid); const post = parse object extend('post'); const postsquery = new parse query(post); postsquery equalto('author', user); postsquery include('author'); postsquery descending('createdat'); const posts = await postsquery find(); const userdata = { id user id, username user get('username'), bio user get('bio') || '', avatar user get('avatar') ? user get('avatar') url() null, followers user get('followers') || 0, following user get('following') || 0, posts posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') })) }; return userdata; } catch (error) { console error('error fetching user profile ', error); throw error; } }; ユーザープロフィールの更新 const updateuserprofile = async (profiledata) => { try { const currentuser = parse user current(); if (profiledata bio !== undefined) { currentuser set('bio', profiledata bio); } if (profiledata avatarfile) { const parsefile = new parse file('avatar jpg', profiledata avatarfile); await parsefile save(); currentuser set('avatar', parsefile); } await currentuser save(); console log('profile updated successfully'); return currentuser; } catch (error) { console error('error updating profile ', error); throw error; } }; ステップ 7 — リアルタイムメッセージングの実装 back4appのlivequery機能は、リアルタイムメッセージングとタイピングインジケーターを可能にします。 会話の作成 const createconversation = async (participantids) => { try { const currentuser = parse user current(); const allparticipantids = \[ new set(\[currentuser id, participantids])]; const existingconversation = await findexistingconversation(allparticipantids); if (existingconversation) { return existingconversation; } const participantpointers = await promise all( allparticipantids map(async (id) => { const userquery = new parse query(parse user); return await userquery get(id); }) ); const conversation = parse object extend('conversation'); const conversation = new conversation(); conversation set('participants', participantpointers); conversation set('lastmessage', ''); await conversation save(); console log('conversation created successfully'); return conversation; } catch (error) { console error('error creating conversation ', error); throw error; } }; // helper to find an existing conversation (simplified for demo) const findexistingconversation = async (participantids) => { try { const conversation = parse object extend('conversation'); const query = new parse query(conversation); const results = await query find(); for (const conversation of results) { const participants = conversation get('participants') || \[]; const ids = participants map(p => p id); if (ids length === participantids length && ids every(id => participantids includes(id))) { return conversation; } } return null; } catch (error) { console error('error finding conversation ', error); return null; } }; メッセージの送信 const sendmessage = async (conversationid, messagetext) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationquery = new parse query(conversation); const conversation = await conversationquery get(conversationid); const message = parse object extend('message'); const message = new message(); message set('text', messagetext); message set('sender', currentuser); message set('conversation', conversation); message set('read', false); await message save(); conversation set('lastmessage', messagetext); await conversation save(); console log('message sent successfully'); return message; } catch (error) { console error('error sending message ', error); throw error; } }; livequeryを介してメッセージ更新を購読する const subscribetomessages = async (conversationid, onnewmessage) => { try { const message = parse object extend('message'); const query = new parse query(message); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('sender'); const subscription = await query subscribe(); subscription on('create', (message) => { const newmessage = { id message id, text message get('text'), createdat message get('createdat'), sender { id message get('sender') id, username message get('sender') get('username'), avatar message get('sender') get('avatar') ? message get('sender') get('avatar') url() null }, read message get('read') }; onnewmessage(newmessage); if (message get('sender') id !== parse user current() id) { markmessageasread(message); } }); return subscription; } catch (error) { console error('error subscribing to messages ', error); throw error; } }; const markmessageasread = async (message) => { try { message set('read', true); await message save(); } catch (error) { console error('error marking message as read ', error); } }; タイピングインジケーターの実装 const updatetypingstatus = async (conversationid, istyping) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); query equalto('conversation', conversationpointer); query equalto('user', currentuser); let typingstatus = await query first(); if (typingstatus) { typingstatus set('istyping', istyping); } else { typingstatus = new typingstatus(); typingstatus set('conversation', conversationpointer); typingstatus set('user', currentuser); typingstatus set('istyping', istyping); } await typingstatus save(); } catch (error) { console error('error updating typing status ', error); } }; const subscribetotypingstatus = async (conversationid, ontypingstatuschange) => { try { const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('user'); const subscription = await query subscribe(); subscription on('update', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); subscription on('create', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); return subscription; } catch (error) { console error('error subscribing to typing status ', error); throw error; } }; ステップ 8 — 検索機能の追加 ユーザー検索 const searchusers = async (query, limit = 20) => { try { const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); const users = await userquery find(); const userresults = users map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null, bio user get('bio') || '' })); return userresults; } catch (error) { console error('error searching users ', error); throw error; } }; 投稿検索 const searchposts = async (query, limit = 20) => { try { const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const postresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), 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 } })); return postresults; } catch (error) { console error('error searching posts ', error); throw error; } }; ハッシュタグ検索 const searchhashtags = async (tag, limit = 20) => { try { const hashtagquery = tag startswith('#') ? tag substring(1) tag; const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(`#${hashtagquery}\\\b`, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const hashtagresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), 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 } })); return hashtagresults; } catch (error) { console error('error searching hashtags ', error); throw error; } }; ステップ 9 — back4appの高度な機能を活用する back4appは、バックエンドを豊かにするためのcloud functions、バックグラウンドジョブ、フックなどの高度なツールを提供します。 サーバーサイドロジックのためのcloud functions // cloud function example to notify users when a comment is added parse cloud aftersave("comment", async (request) => { if (request original) return; const comment = request object; const post = comment get("post"); const commenter = request user; const postquery = new parse query("post"); const fullpost = await postquery get(post id, { usemasterkey true }); const postauthor = fullpost get("author"); if (postauthor id === commenter id) return; const notification = parse object extend("notification"); const notification = new notification(); notification set("type", "comment"); notification set("fromuser", commenter); notification set("touser", postauthor); notification set("post", post); notification set("read", false); await notification save(null, { usemasterkey true }); }); cloud codeを使用した高度な検索 parse cloud define("advancedsearch", async (request) => { const { query, type, limit = 20 } = request params; if (!query) { throw new error("search query is required"); } let results = \[]; switch (type) { case 'users' const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); results = await userquery find({ usemasterkey true }); break; case 'posts' const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery limit(limit); results = await postquery find({ usemasterkey true }); break; default throw new error("invalid search type"); } return results; }); 定期的なタスクのためのバックグラウンドジョブ parse cloud job("calculatetrendingtopics", async () => { const post = parse object extend("post"); const query = new parse query(post); const oneweekago = new date(); oneweekago setdate(oneweekago getdate() 7); query greaterthan('createdat', oneweekago); query limit(1000); const posts = await query find({ usemasterkey true }); const hashtagcounts = {}; posts foreach(post => { const content = post get('content') || ''; const hashtags = content match(/#(\w+)/g) || \[]; hashtags foreach(hashtag => { const tag = hashtag tolowercase(); hashtagcounts\[tag] = (hashtagcounts\[tag] || 0) + 1; }); }); const trendingarray = object entries(hashtagcounts) map((\[hashtag, count]) => ({ hashtag, count })) sort((a, b) => b count a count) slice(0, 10); const trendingtopics = parse object extend("trendingtopics"); const trending = new trendingtopics(); trending set("topics", trendingarray); trending set("calculatedat", new date()); await trending save(null, { usemasterkey true }); return "trending topics calculated successfully"; }); 追加の高度な機能 プッシュ通知: back4appのプッシュサービスを使用して、新しいメッセージについてユーザーに通知します。 役割ベースのアクセス制御: コンテンツのモデレーションなどのアクションに対する権限を管理するために役割を定義します。 ウェブフック: cloud codeからウェブフックをトリガーして外部サービスを統合します。 ステップ10 — パフォーマンス最適化 バックエンドがスケールするにつれて、パフォーマンス調整が重要です。以下の技術を考慮してください: データベースインデックス 頻繁にクエリされるフィールド(例: author , createdat , username )にインデックスを作成して、クエリパフォーマンスを向上させます。 クエリ最適化 必要なフィールドのみを選択し、結果セットを制限するためにページネーションを使用してクエリを最適化します。 バッチ操作 apiコールを最小限に抑えるために、バッチ更新と削除を利用します。 const updatemultipleposts = async (postids, updatedata) => { try { const post = parse object extend('post'); const posts = postids map(id => { const post = new post(); post id = id; return post; }); posts foreach(post => { object entries(updatedata) foreach((\[key, value]) => { post set(key, value); }); }); await parse object saveall(posts); console log('posts updated successfully'); } catch (error) { console error('error updating posts ', error); throw error; } }; キャッシング戦略 apiリクエストを減らすために、頻繁にアクセスされるデータのクライアントサイドキャッシングを実装します。 効率的なlivequeryの使用 必要なデータのみにサブスクライブし、データが不要になったら常にサブスクリプションを解除します。 ファイル処理の最適化 帯域幅とストレージを節約するために、アップロード前に画像のサイズを変更します。 const uploadresizedimage = async (originalfile, maxwidth = 1200, maxheight = 1200) => { return new promise((resolve, reject) => { try { const reader = new filereader(); reader onload = (event) => { const img = new image(); img onload = () => { let width = img width; let height = img height; if (width > maxwidth) { height = math round(height (maxwidth / width)); width = maxwidth; } if (height > maxheight) { width = math round(width (maxheight / height)); height = maxheight; } const canvas = document createelement('canvas'); canvas width = width; canvas height = height; const ctx = canvas getcontext('2d'); ctx drawimage(img, 0, 0, width, height); canvas toblob(async (blob) => { const resizedfile = new file(\[blob], originalfile name, { type originalfile type, lastmodified date now() }); const parsefile = new parse file(resizedfile name, resizedfile); await parsefile save(); resolve(parsefile); }, originalfile type, 0 8); }; img src = event target result; }; reader readasdataurl(originalfile); } catch (error) { reject(error); } }); }; 監視とスケーリング back4appのダッシュボード分析とログを使用して、apiの使用状況とパフォーマンスを監視します。 ユーザーベースが増えるにつれて、プランのアップグレードやシャーディング、cdnソリューションの実装を検討してください。 結論 このチュートリアルでは、back4appを使用してソーシャルネットワークのバックエンドを設定する方法を学びました。ユーザー認証、投稿管理、リアルタイムメッセージング、検索機能、そして高度な最適化技術をカバーしました。 また、私たちのブログ記事もお読みください。 ソーシャルメディアアプリの開発方法 について詳しく説明しています。 コーディングを楽しんで、ソーシャルネットワークのバックエンドでの成功を祈っています!