Project Templates
Social Network
あなたのソーシャルネットワークのリアルタイムチャット
49 分
イントロダクション このチュートリアルでは、back4appを使用してソーシャルネットワークアプリケーションのリアルタイムメッセージングシステムを実装する方法を学びます。ユーザーがメッセージを即座に送受信し、タイピングインジケーターを表示し、会話を管理できる完全なチャット機能を構築します。これは、魅力的なソーシャルプラットフォームに不可欠な機能です。 back4appは、parse server上に構築されたbackend as a service(baas)プラットフォームで、live query機能を通じて強力なリアルタイム機能を提供します。back4appのリアルタイムインフラストラクチャを使用すると、複雑なwebsocketサーバーやスケーリングの懸念を管理することなく、応答性の高いメッセージングシステムを作成できます。 このチュートリアルの終わりまでに、ソーシャルネットワークアプリケーションであるback4gramで使用されているものに似た完全に機能するメッセージングシステムを作成します。会話の作成、リアルタイムメッセージ交換、タイピングインジケーター、ユーザー検索を実装し、ユーザーにシームレスなコミュニケーション体験を提供します。 back4gramプロジェクト: 見つける こちら 完全なコードを ソーシャルネットワークサンプルプロジェクト back4appで構築された 前提条件 このチュートリアルを完了するには、次のものが必要です: back4appアカウント。 back4app com https //www back4app com で無料アカウントにサインアップできます。 parse javascript sdkが初期化されたback4appプロジェクト。 ローカルマシンにnode jsがインストールされていること。 javascript、react js、back4app/parse serverの基本的な知識。 すでに実装された認証システム。まだ設定していない場合は、 認証システムチュートリアル https //www back4app com/docs/react/authentication tutorial を確認してください。 reactフックとコンポーネントライフサイクルに慣れていること。 ステップ1 – back4appのリアルタイム機能の理解 コーディングを始める前に、back4appがどのようにlive query機能を通じてリアルタイム機能を実現しているかを理解しましょう。 live queryの説明 live queryは、クライアントがクエリにサブスクライブし、そのクエリに一致するオブジェクトが作成、更新、または削除されたときに更新を受け取ることを可能にするparse serverの機能です。これは、チャットシステムのようなリアルタイムアプリケーションを構築するのに最適です。 live queryの動作は次のとおりです: クライアントは特定のクエリ(例:"会話xのすべてのメッセージ")にサブスクライブします。 サーバー上で一致するオブジェクトが作成、更新、または削除されると、 live queryは自動的にすべてのサブスクライブされたクライアントに通知します。 クライアントはこれらのイベントに応じてuiを更新できます。 メッセージングシステムの文脈では、これは意味します: 新しいメッセージが送信されると、その会話にいるすべてのユーザーが即座に受け取ります ユーザーが入力を開始すると、他のユーザーはリアルタイムで入力インジケーターを見ることができます メッセージが読まれると、すべての参加者のために既読通知が更新されることがあります back4appでのライブクエリの設定 ライブクエリを使用するには、back4appダッシュボードでそれを有効にする必要があります: back4appダッシュボードにログインします サーバー設定 > ウェブホスティングとライブクエリに移動します ライブクエリを有効にします ライブクエリで使用したいクラスを追加します(この場合は「メッセージ」と「タイピングステータス」) 次に、クライアントサイドのコードでlive queryクライアントを初期化する必要があります: // initialize parse with your back4app credentials parse initialize("your app id", "your javascript key"); parse serverurl = "https //parseapi back4app com/"; // initialize live query parse livequeryserverurl = "wss\ //your app id back4app io"; 次に、メッセージングシステムのデータベーススキーマを設計します。 back4gramプロジェクト: を見つける こちら の完全なコードは、 ソーシャルネットワークのサンプルプロジェクト back4appで構築されています。 ステップ2 – データベーススキーマの設計 メッセージングシステムには、いくつかのparseクラスが必要です: 会話 2人以上のユーザー間のチャットを表します メッセージ 会話内の個々のメッセージを表します タイピングステータス ユーザーが会話中にタイピングしているときの状態を追跡します これらのクラスをback4appで作成しましょう: 会話クラス 会話クラスには次のフィールドがあります: 参加者 (ユーザーへのポインタの配列):会話に参加しているユーザー 最後のメッセージ (文字列):最も最近のメッセージのテキスト 更新日時 (日付):レコードが変更されるとparseによって自動的に更新されます メッセージクラス メッセージクラスには次のものがあります: 会話 (会話へのポインタ):このメッセージが属する会話 送信者 (ユーザーへのポインタ):メッセージを送信したユーザー テキスト (文字列):メッセージの内容 作成日時 (日付):メッセージが送信されるとparseによって自動的に作成されます typingstatus クラス typingstatus クラスは、タイピングインジケーターを追跡します: ユーザー (ユーザーへのポインタ):タイピングしているユーザー 会話 (会話へのポインタ):タイピングが行われている会話 istyping (ブール値):ユーザーが現在タイピングしているかどうか では、このメッセージングシステムを実装するためにプロジェクト構造を設定しましょう。 back4gram プロジェクト: 見つける こちら 完全なコードを ソーシャルネットワークのサンプルプロジェクト back4app で構築された ステップ 3 – プロジェクト構造の設定 メッセージングシステムのコードを整理しましょう。重要なコンポーネントに焦点を当てます src/ ├── components/ │ └── ui/ │ ├── toaster js │ └── avatar js ├── pages/ │ └── messagespage js ├── app js └── parseconfig js シンプルな toaster js を通知用に作成しましょう // src/components/ui/toaster js export const toaster = { create ({ title, description, type }) => { // in a real app, you would use a proper toast notification component console log(`\[${type touppercase()}] ${title} ${description}`); alert(`${title} ${description}`); } }; そして、私たちの app js をメッセージルートを含めるように更新します // src/app js import react from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import messagespage from ' /pages/messagespage'; // import other pages function app() { return ( \<router> \<routes> \<route path="/messages" element={\<messagespage />} /> {/ other routes /} \</routes> \</router> ); } export default app; さて、メインのメッセージ機能を実装しましょう。 back4gramプロジェクト 完全なコードを見つけるには こちら の ソーシャルネットワークサンプルプロジェクト をback4appで構築しました。 ステップ 4 – メッセージページコンポーネントの作成 私たちの messagespage js コンポーネントはメッセージングシステムの中心になります。それは 会話のリストを表示する ユーザーがメッセージを表示および送信できるようにする 入力中のインジケーターを表示する 新しい会話を始めるためにユーザーを検索できるようにする これを段階的に構築しましょう: // src/pages/messagespage js import react, { usestate, useeffect, useref } from 'react'; import { box, flex, input, button, vstack, text, avatar, heading, spinner, center, hstack, divider, } from '@chakra ui/react'; import { usenavigate } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function messagespage() { const \[message, setmessage] = usestate(''); const \[istyping, setistyping] = usestate(false); const \[isloading, setisloading] = usestate(true); const \[issending, setissending] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); const \[conversations, setconversations] = usestate(\[]); const \[activeconversation, setactiveconversation] = usestate(null); const \[messages, setmessages] = usestate(\[]); const \[users, setusers] = usestate(\[]); const \[searchquery, setsearchquery] = usestate(''); const \[showusersearch, setshowusersearch] = usestate(false); const \[processedmessageids, setprocessedmessageids] = usestate(new set()); const \[otherusertyping, setotherusertyping] = usestate(false); const messagesendref = useref(null); const livequerysubscription = useref(null); const typingstatussubscription = useref(null); const typingtimerref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { console log('checking authentication '); const user = await parse user current(); if (!user) { console log('no user found, redirecting to login'); navigate('/login'); return; } console log('user authenticated ', user id, user get('username')); setcurrentuser(user); fetchconversations(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); // clean up subscriptions when component unmounts return () => { if (livequerysubscription current) { livequerysubscription current unsubscribe(); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); } if (typingtimerref current) { cleartimeout(typingtimerref current); } }; }, \[navigate]); // we'll implement the following functions next const fetchconversations = async (user) => { // coming up next }; const resetmessagestate = () => { // coming up next }; const fetchmessages = async (conversationid) => { // coming up next }; const setuplivequery = async (conversationid) => { // coming up next }; const setuptypingstatussubscription = async (conversationid) => { // coming up next }; const updatetypingstatus = async (istyping) => { // coming up next }; const sendmessage = async () => { // coming up next }; const searchusers = async (query) => { // coming up next }; const startconversation = async (userid) => { // coming up next }; // format time for messages const formatmessagetime = (date) => { return new date(date) tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); }; // format date for conversation list const formatconversationdate = (date) => { const now = new date(); const messagedate = new date(date); // if today, show time if (messagedate todatestring() === now\ todatestring()) { return messagedate tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); } // if this year, show month and day if (messagedate getfullyear() === now\ getfullyear()) { return messagedate tolocaledatestring(\[], { month 'short', day 'numeric' }); } // otherwise show full date return messagedate tolocaledatestring(); }; return ( \<flex h="100vh"> {/ we'll implement the ui next /} \</flex> ); } export default messagespage; さあ、私たちのメッセージングシステムに必要な各関数を実装しましょう。 back4gramプロジェクト: 見つける こちら 完全なコードは ソーシャルネットワークサンプルプロジェクト back4appで構築されています。 ステップ 5 – ユーザーの会話を取得する まず、現在のユーザーのすべての会話を読み込む fetchconversations 関数を実装しましょう const fetchconversations = async (user) => { setisloading(true); try { console log('fetching conversations for user ', user id); // query conversations where the current user is a participant const query = new parse query('conversation'); query equalto('participants', user); query include('participants'); query descending('updatedat'); const results = await query find(); console log('found conversations ', results length); // format conversations const formattedconversations = results map(conv => { const participants = conv get('participants'); // find the other participant (not the current user) const otherparticipant = participants find(p => p id !== user id); if (!otherparticipant) { console warn('could not find other participant in conversation ', conv id); return null; } return { id conv id, user { id otherparticipant id, username otherparticipant get('username'), avatar otherparticipant get('avatar') ? otherparticipant get('avatar') url() null }, lastmessage conv get('lastmessage') || '', updatedat conv get('updatedat') }; }) filter(boolean); // remove any null entries console log('formatted conversations ', formattedconversations); setconversations(formattedconversations); // if there are conversations, set the first one as active if (formattedconversations length > 0) { setactiveconversation(formattedconversations\[0]); fetchmessages(formattedconversations\[0] id); } } catch (error) { console error('error fetching conversations ', error); toaster create({ title 'error', description 'failed to load conversations', type 'error', }); } finally { setisloading(false); } }; この関数 現在のユーザーが参加者である会話のために conversation クラスをクエリする 結果を ui に表示するためにフォーマットする 利用可能な場合、最初の会話をアクティブに設定する back4gramプロジェクト: を見つける こちら 完全なコードを ソーシャルネットワークのサンプルプロジェクト back4appで構築された ステップ6 – メッセージの処理とリアルタイム更新 では、メッセージを取得し、live queryを使用してリアルタイム更新を設定する関数を実装しましょう: const resetmessagestate = () => { console log('resetting message state'); setmessages(\[]); setprocessedmessageids(new set()); setotherusertyping(false); if (livequerysubscription current) { livequerysubscription current unsubscribe(); livequerysubscription current = null; console log('unsubscribed from live query in resetmessagestate'); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); typingstatussubscription current = null; console log('unsubscribed from typing status subscription in resetmessagestate'); } }; const fetchmessages = async (conversationid) => { // reset message state to avoid any lingering messages or subscriptions resetmessagestate(); try { // query messages for this conversation const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query ascending('createdat'); const results = await query find(); // format messages const formattedmessages = results map(msg => ({ id msg id, text msg get('text'), sender { id msg get('sender') id, username msg get('sender') get('username') }, createdat msg get('createdat') })); // initialize the set of processed message ids const messageids = new set(formattedmessages map(msg => msg id)); // set state after processing all messages setmessages(formattedmessages); setprocessedmessageids(messageids); // set up live query subscription for new messages setuplivequery(conversationid); // set up typing status subscription setuptypingstatussubscription(conversationid); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } catch (error) { console error('error fetching messages ', error); toaster create({ title 'error', description 'failed to load messages', type 'error', }); } }; では、リアルタイムメッセージ更新のためのlive queryの購読を実装しましょう: const setuplivequery = async (conversationid) => { // capture the current user in a closure to avoid null reference later const captureduser = currentuser; // unsubscribe from previous subscription if exists if (livequerysubscription current) { livequerysubscription current unsubscribe(); console log('unsubscribed from previous live query'); } try { console log('setting up live query for conversation ', conversationid); // create a query that will be used for the subscription const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); console log('created query for message class with conversation id ', conversationid); // 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 for conversation ', conversationid); }); // handle new messages livequerysubscription current on('create', (message) => { console log('new message received via live query ', message id); // check if we've already processed this message if (processedmessageids has(message id)) { console log('skipping duplicate message ', message id); return; } // format the new message const newmessage = { id message id, text message get('text'), sender { id message get('sender') id, username message get('sender') get('username') }, createdat message get('createdat') }; console log('formatted new message ', newmessage); // add the message id to the set of processed ids setprocessedmessageids(previds => { const newids = new set(previds); newids add(message id); return newids; }); // add the new message to the messages state setmessages(prevmessages => { // check if the message is already in the list (additional duplicate check) if (prevmessages some(msg => msg id === message id)) { console log('message already in list, not adding again ', message id); return prevmessages; } return \[ prevmessages, newmessage]; }); // use the captured user to avoid null reference if (captureduser && message get('sender') id !== captureduser id) { setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } else { // if user check fails, update without the check setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); // handle subscription close livequerysubscription current on('close', () => { console log('live query connection closed for conversation ', conversationid); }); } catch (error) { console error('error setting up live query ', error); toaster create({ title 'error', description 'failed to set up real time messaging', type 'error', }); } }; このライブクエリの設定: 現在の会話で新しいメッセージのサブスクリプションを作成します リアルタイムで受信メッセージを処理します 新しいメッセージが到着するたびにuiを更新します 最新のメッセージとタイムスタンプで会話リストを更新します 重複メッセージを管理します(これはlive queryを介して送信と受信の両方を行うときに発生する可能性があります) back4gramプロジェクト: 見つける こちら 完全なコードを ソーシャルネットワークサンプルプロジェクト back4appで構築された ステップ 7 – タイピングインジケーターの実装 ユーザーエクスペリエンスを向上させるために、リアルタイムのタイピングインジケーターを追加しましょう: const setuptypingstatussubscription = async (conversationid) => { // cancel previous subscription, if it exists if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); console log('unsubscribed from previous typing status subscription'); } try { console log('setting up typing status subscription for conversation ', conversationid); // create a query for the typingstatus class const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = conversationid; // filter by conversation query equalto('conversation', conversation); // don't include the current user's status query notequalto('user', currentuser); // subscribe to the query typingstatussubscription current = await query subscribe(); console log('successfully subscribed to typing status'); // handle create events typingstatussubscription current on('create', (status) => { console log('typing status created ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle update events typingstatussubscription current on('update', (status) => { console log('typing status updated ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle errors typingstatussubscription current on('error', (error) => { console error('typing status subscription error ', error); }); } catch (error) { console error('error setting up typing status subscription ', error); } }; const updatetypingstatus = async (istyping) => { if (!activeconversation || !currentuser) return; try { // check if a typing status already exists for this user and conversation const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = activeconversation id; query equalto('user', currentuser); query equalto('conversation', conversation); const existingstatus = await query first(); if (existingstatus) { // update existing status existingstatus set('istyping', istyping); await existingstatus save(); } else { // create a new status const typingstatus = parse object extend('typingstatus'); const newstatus = new typingstatus(); newstatus set('user', currentuser); newstatus set('conversation', conversation); newstatus set('istyping', istyping); await newstatus save(); } } catch (error) { console error('error updating typing status ', error); } }; この実装: 会話中の他のユーザーからの入力状況の更新を購読する 他の誰かが入力しているときにuiを更新する 現在のユーザーが入力しているときに入力状況の更新を送信する back4gramプロジェクト: 見つける こちら 完全なコードを ソーシャルネットワークサンプルプロジェクト back4appで構築された ステップ8 – メッセージの送信 では、メッセージ送信機能を実装しましょう: const sendmessage = async () => { if (!message trim() || !activeconversation) return; const messagetext = message trim(); // store the message text setissending(true); setmessage(''); // clear input immediately to prevent double sending setistyping(false); // clear typing status locally updatetypingstatus(false); // clear typing status on server // clear typing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } try { // create message const message = parse object extend('message'); const newmessage = new message(); // set conversation pointer const conversation = new parse object('conversation'); conversation id = activeconversation id; newmessage set('conversation', conversation); newmessage set('sender', currentuser); newmessage set('text', messagetext); // save the message const savedmessage = await newmessage save(); console log('message saved ', savedmessage id); // add the message id to the set of processed ids to prevent duplication // when it comes back through live query setprocessedmessageids(previds => { const newids = new set(previds); newids add(savedmessage id); return newids; }); // check if this is a new conversation (no messages yet) // if it is, add the message to the ui immediately if (messages length === 0) { console log('first message in conversation, adding to ui immediately'); const formattedmessage = { id savedmessage id, text messagetext, sender { id currentuser id, username currentuser get('username') }, createdat new date() }; setmessages(\[formattedmessage]); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } // update conversation's lastmessage const conversationobj = await new parse query('conversation') get(activeconversation id); conversationobj set('lastmessage', messagetext); await conversationobj save(); // update the conversation in the list setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === activeconversation id) { return { conv, lastmessage messagetext, updatedat new date() }; } return conv; }); }); } catch (error) { console error('error sending message ', error); toaster create({ title 'error', description 'failed to send message', type 'error', }); // if there's an error, put the message back in the input setmessage(messagetext); } finally { setissending(false); } }; この関数 back4appデータベースに新しいメッセージを作成して保存します 会話の最後のメッセージとタイムスタンプを更新します live queryを介してメッセージが戻ってくる際の重複防止を処理します 送信者に即時フィードバックを提供します back4gramプロジェクト: こちらを見つけてください ここ の完全なコードを見つけてください ソーシャルネットワークサンプルプロジェクト はback4appで構築されています ステップ9 – ユーザー検索と新しい会話の開始 ユーザーを検索し、新しい会話を始める機能を実装しましょう: const searchusers = async (query) => { if (!query trim()) { setusers(\[]); return; } try { console log('searching for users with query ', query); // create a query for the user class const userquery = new parse query(parse user); // search for username containing the query string (case insensitive) userquery contains('username', query tolowercase()); // don't include the current user in results userquery notequalto('objectid', currentuser id); // limit to 10 results userquery limit(10); // execute the query const results = await userquery find(); console log('user search results ', results length); // format users const formattedusers = results map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null })); console log('formatted users ', formattedusers); setusers(formattedusers); } catch (error) { console error('error searching users ', error); toaster create({ title 'error', description 'failed to search users', type 'error', }); } }; const startconversation = async (userid) => { console log('starting conversation with user id ', userid); try { // check if a conversation already exists with this user const query = new parse query('conversation'); // create pointers to both users const currentuserpointer = parse user current(); const otheruserpointer = new parse user(); otheruserpointer id = userid; // find conversations where both users are participants query containsall('participants', \[currentuserpointer, otheruserpointer]); const existingconv = await query first(); if (existingconv) { console log('found existing conversation ', existingconv id); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // format the conversation const conversation = { id existingconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage existingconv get('lastmessage') || '', updatedat existingconv get('updatedat') }; // set as active conversation setactiveconversation(conversation); // fetch messages and set up live query await fetchmessages(conversation id); } else { console log('creating new conversation with user ', userid); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // create a new conversation const conversation = parse object extend('conversation'); const newconversation = new conversation(); // set participants newconversation set('participants', \[currentuserpointer, otheruserpointer]); newconversation set('lastmessage', ''); // save the conversation const savedconv = await newconversation save(); console log('new conversation created ', savedconv id); // format the conversation const conversation = { id savedconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage '', updatedat savedconv get('updatedat') }; // add to conversations list setconversations(prev => \[conversation, prev]); // set as active conversation and reset message state setactiveconversation(conversation); resetmessagestate(); // set up live query for the new conversation await setuplivequery(savedconv id); // set up typing status subscription await setuptypingstatussubscription(savedconv id); // reset search setshowusersearch(false); setsearchquery(''); setusers(\[]); } } catch (error) { console error('error starting conversation ', error); toaster create({ title 'error', description 'failed to start conversation', type 'error', }); } }; このコードは、私たちのメッセージングシステムにとって重要な2つの機能を提供します: ユーザー検索 searchusers 機能は、ユーザーがユーザー名で他のユーザーを見つけることを可能にします。これはback4appのuserクラスをクエリし、現在のユーザーを結果から除外し、表示用にデータをフォーマットします。 会話の作成 startconversation 機能は、既存の会話を見つけることと新しい会話を作成することの両方を処理します。 ユーザー間に既に会話が存在するか確認します 存在する場合、その会話とメッセージを読み込みます 会話が存在しない場合、新しい会話を作成します 新しいメッセージと入力状況のためのライブクエリのサブスクリプションを設定します これらの機能が実装されることで、ユーザーは他のユーザーを簡単に見つけて会話を始めることができ、これはソーシャルネットワークのメッセージングシステムにとって不可欠です。 back4gramプロジェクト 完全なコードを見つけるには、 こちら を参照してください。 ソーシャルネットワークのサンプルプロジェクト はback4appで構築されています。 ステップ10 – チャットuiの構築 すべての機能を実装したので、メッセージングシステムのユーザーインターフェースを作成しましょう。uiは次のようになります。 すべての会話を一覧表示するサイドバー ユーザーを見つけるための検索インターフェース メッセージを表示するチャットウィンドウ 入力中のインジケーターサポートを持つメッセージ入力 こちらが私たちのmessagespageコンポーネントのjsxです: return ( \<flex h="100vh"> {/ chat list sidebar /} \<box w="300px" borderrightwidth="1px" bordercolor="gray 600" p={4} bg="gray 800"> \<heading size="md" mb={4}> messages \</heading> \<flex mb={4}> \<input placeholder="search users " value={searchquery} onchange={(e) => { const value = e target value; setsearchquery(value); if (value trim() length > 0) { searchusers(value); } else { setusers(\[]); } }} mr={2} /> \<button onclick={() => { setshowusersearch(!showusersearch); if (!showusersearch) { setsearchquery(''); setusers(\[]); } }} \> {showusersearch ? 'cancel' 'new'} \</button> \</flex> {isloading ? ( \<center py={10}> \<spinner size="lg" /> \</center> ) showusersearch ? ( // user search results \<vstack align="stretch" spacing={2}> {users length > 0 ? ( users map(user => ( \<box key={user id} p={3} borderradius="md" hover={{ bg "gray 700" }} cursor="pointer" onclick={() => startconversation(user id)} \> \<hstack> \<avatar size="sm" name={user username} src={user avatar} /> \<text>{user username}\</text> \</hstack> \</box> )) ) searchquery ? ( \<text color="gray 400" textalign="center">no users found\</text> ) ( \<text color="gray 400" textalign="center">search for users to message\</text> )} \</vstack> ) ( // conversations list \<vstack align="stretch" spacing={0}> {conversations length > 0 ? ( conversations map(conv => ( \<box key={conv id} p={3} borderradius="md" bg={activeconversation? id === conv id ? "gray 700" "transparent"} hover={{ bg "gray 700" }} cursor="pointer" onclick={() => { if (activeconversation? id !== conv id) { setactiveconversation(conv); fetchmessages(conv id); } }} \> \<hstack> \<avatar size="sm" name={conv user username} src={conv user avatar} /> \<box flex="1" overflow="hidden"> \<flex justify="space between" align="center"> \<text fontweight="bold" nooflines={1}>{conv user username}\</text> \<text fontsize="xs" color="gray 400"> {formatconversationdate(conv updatedat)} \</text> \</flex> \<text fontsize="sm" color="gray 400" nooflines={1}> {conv lastmessage || 'no messages yet'} \</text> \</box> \</hstack> \</box> )) ) ( \<text color="gray 400" textalign="center" mt={8}> no conversations yet start a new one! \</text> )} \</vstack> )} \</box> {/ chat window /} \<box flex={1} p={0} display="flex" flexdirection="column" bg="gray 900"> {activeconversation ? ( <> {/ chat header /} \<flex align="center" p={4} borderbottomwidth="1px" bordercolor="gray 700"> \<avatar size="sm" mr={2} name={activeconversation user username} src={activeconversation user avatar} /> \<text fontweight="bold">{activeconversation user username}\</text> {otherusertyping && ( \<text ml={2} color="gray 400" fontsize="sm"> is typing \</text> )} \</flex> {/ messages /} \<box flex={1} p={4} overflowy="auto" display="flex" flexdirection="column" \> {messages length > 0 ? ( messages map((msg) => ( \<box key={msg id} alignself={msg sender id === currentuser id ? "flex end" "flex start"} bg={msg sender id === currentuser id ? "blue 500" "gray 700"} color={msg sender id === currentuser id ? "white" "white"} p={3} borderradius="lg" maxw="70%" mb={2} \> \<text>{msg text}\</text> \<text fontsize="xs" color={msg sender id === currentuser id ? "blue 100" "gray 400"} textalign="right" mt={1}> {formatmessagetime(msg createdat)} \</text> \</box> )) ) ( \<center flex={1}> \<text color="gray 400"> no messages yet say hello! \</text> \</center> )} \<div ref={messagesendref} /> \</box> {/ message input /} \<flex p={4} bordertopwidth="1px" bordercolor="gray 700"> \<input placeholder="type a message " value={message} onchange={(e) => { setmessage(e target value); // set typing status to true locally setistyping(true); // update typing status on server updatetypingstatus(true); // clear any existing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } // set a new timer to turn off typing status after 2 seconds of inactivity typingtimerref current = settimeout(() => { setistyping(false); updatetypingstatus(false); }, 2000); }} mr={2} onkeypress={(e) => { if (e key === 'enter' && !e shiftkey) { e preventdefault(); sendmessage(); // also clear typing status when sending a message setistyping(false); updatetypingstatus(false); if (typingtimerref current) { cleartimeout(typingtimerref current); } } }} /> \<button colorscheme="blue" onclick={sendmessage} isloading={issending} disabled={!message trim() || issending} \> send \</button> \</flex> \</> ) ( \<center flex={1}> \<vstack> \<text fontsize="xl" fontweight="bold">welcome to messages\</text> \<text color="gray 400"> select a conversation or start a new one \</text> \</vstack> \</center> )} \</box> \</flex> ); uiを主要なコンポーネントに分解しましょう: 会話サイドバー 左のサイドバーにはすべての会話のリストが表示されます 各会話には他のユーザーの名前、アバター、最後のメッセージが表示されます ユーザーは任意の会話をクリックしてチャットを表示し続けることができます 上部の検索バーを使用して他のユーザーを見つけ、新しい会話を開始できます メッセージ表示 メインエリアにはアクティブな会話のメッセージが表示されます 現在のユーザーからのメッセージは右側に青い背景で表示されます 他のユーザーからのメッセージは左側に灰色の背景で表示されます 各メッセージにはメッセージのテキストと送信された時間が表示されます 新しいメッセージが届くと、メッセージリストは自動的に下にスクロールします メッセージ入力 下部にはメッセージを入力して送信するための入力エリアがあります ユーザーはenterキーを押してメッセージを送信できます 送信ボタンもメッセージを送信することができます 入力エリアに入力すると、自動的に入力インジケーターがトリガーされます 会話が選択されるまで入力は無効になります タイピングインジケーター 他のユーザーがタイピングしているとき、「タイピング中 」のインジケーターが表示されます インジケーターは、typingstatus live queryサブスクリプションを使用してリアルタイムで更新されます このuiは、人気のメッセージングアプリケーションからユーザーが期待するような、クリーンで直感的なメッセージングインターフェースを提供します。 back4gramプロジェクト: こちらで こちら 完全なコードを見つけてください ソーシャルネットワークサンプルプロジェクト back4appで構築された ステップ11 – ライブクエリ接続のテスト 私たちのライブクエリ接続が正しく機能していることを確認するために、コンポーネントがマウントされたときに接続を検証するテスト関数を追加しましょう: // add this at the beginning of the component to test live query connection useeffect(() => { // test live query connection const testlivequery = async () => { try { console log('testing live query connection '); console log('live query url ', parse livequeryserverurl); const query = new parse query('message'); console log('created test query for message class'); const subscription = await query subscribe(); console log('live query subscription successful!'); subscription on('open', () => { console log('live query connection opened successfully'); }); subscription on('create', (object) => { console log('live query create event received ', object id); }); subscription on('error', (error) => { console error('live query error ', error); }); // unsubscribe after a few seconds to test settimeout(() => { subscription unsubscribe(); console log('unsubscribed from test live query'); }, 10000); } catch (error) { console error('error testing live query ', error); } }; testlivequery(); }, \[]); このテスト関数: messageクラスへのサブスクリプションを作成しようとします 接続イベントのためのイベントハンドラを設定します 各ステップの結果をログに記録します 10秒後に自動的にサブスクリプションを解除します コンソールログを監視することで、次のことを確認できます: ライブクエリurlが正しく設定されています 接続が確立できます イベントが適切に受信されています ライブクエリ接続に問題が発生した場合は、以下を確認してください: back4appライブクエリがあなたのアプリに対して有効になっています ライブクエリurlがparse初期化で正しく設定されています メッセージクラスがback4appのライブクエリクラスリストに追加されています あなたのネットワークはwebsocket接続を許可しています back4gramプロジェクト: 見つける こちら 完全なコードを ソーシャルネットワークサンプルプロジェクト back4appで構築されています ステップ12 – back4appがリアルタイムメッセージングをどのように可能にするかの理解 完全なメッセージングシステムを構築したので、back4appの機能がリアルタイムメッセージングをどのように可能にするかを深く見ていきましょう。 ライブクエリインフラストラクチャ back4appのライブクエリ機能はwebsocketに基づいており、クライアントとサーバーの間に持続的な接続を提供します。これは、従来のhttpリクエスト/レスポンスモデルとは根本的に異なります: 従来のrest api クライアントがリクエストを送信 → サーバーが応答 → 接続が閉じる websocket/ライブクエリ クライアントが持続的な接続を確立 → サーバーはいつでも更新をプッシュできる この持続的な接続が、私たちのメッセージングシステムにおけるリアルタイム機能を可能にしています。新しいメッセージが作成されると、back4appはサーバーをポーリングすることなく、すべての購読クライアントにその更新を自動的にプッシュします。 parse serverのサブスクリプションモデル parse serverのサブスクリプションモデルはクエリベースです。これは意味します: 特定のクエリ(例:"会話xのすべてのメッセージ")にサブスクライブします そのクエリに一致するオブジェクトの更新のみを受け取ります さまざまなタイプのイベントを受け取ることができます( 作成 , 更新 , 削除 , など) このクエリベースのアプローチは非常に強力です。なぜなら、正確なサブスクリプションを可能にするからです。私たちのメッセージングシステムでは、アクティブな会話のメッセージにのみサブスクライブしており、すべてのメッセージにサブスクライブするよりもはるかに効率的です。 データベースの考慮事項 back4appでlive queryを使用する際には、いくつかの重要なデータベースの考慮事項があります: インデックス作成 サブスクリプションクエリで使用されるフィールドがインデックスされていることを確認し、パフォーマンスを向上させる データサイズ オブジェクトを小さく保ち、ペイロードサイズを削減し、パフォーマンスを向上させる スケーリング ライブクエリ接続はリソースを使用するため、アプリケーションをスケーリングする際に考慮してください 最適なパフォーマンスのために考慮すべきこと: メッセージクラスの 会話 フィールドにインデックスを作成する 大きなオブジェクトを埋め込むのではなく、ポインタを使用する メモリリークを避けるためにサブスクリプションのライフサイクルを管理する セキュリティの考慮事項 リアルタイムメッセージングを実装する際、セキュリティは重要です: aclsとclp アクセス制御リストとクラスレベルの権限を使用してデータを保護します 接続認証 認証されたユーザーのみがライブクエリ接続を確立できるようにします データ検証 cloud codeを使用してサーバー側でメッセージの内容を検証します レート制限 悪用を防ぐためにレート制限を実装します 例えば、cloud codeを使用してメッセージが保存される前に検証およびサニタイズすることを検討するかもしれません: // in cloud code parse cloud beforesave("message", async (request) => { const message = request object; const text = message get("text"); // check if message is empty if (!text || text trim() length === 0) { throw new error("message cannot be empty"); } // sanitize message text message set("text", sanitizehtml(text)); // rate limiting check if user is sending too many messages const sender = message get("sender"); const query = new parse query("message"); query equalto("sender", sender); query greaterthan("createdat", new date(new date() 60000)); // last minute const count = await query count({usemasterkey true}); if (count > 10) { throw new error("you are sending messages too quickly please slow down "); } }); back4gramプロジェクト: 完全なコードを見つけるには、 こちら で、 ソーシャルネットワークのサンプルプロジェクト を構築したback4appの完全なコードを見つけてください。 ステップ13 – メッセージングシステムのテストと最適化 メッセージングシステムが正しく機能することを確認するために、徹底的にテストする必要があります: 基本機能テスト メッセージ送信 メッセージが送信され、受信できることを確認します 会話作成 新しい会話を作成するテストを行います ユーザー検索 ユーザーが見つかり、会話が開始できることを確認します uiの更新 タイピングインジケーター、メッセージリスト、会話リストが正しく更新されることを確認します マルチユーザーテスト メッセージングシステムを適切にテストするには、複数のユーザーでテストする必要があります: 2つのブラウザウィンドウまたはタブを開く それぞれ異なるユーザーとしてログインする 彼らの間で会話を始める メッセージを行き来させる タイピングインジケーターとリアルタイム更新をテストする パフォーマンス最適化 大規模なアプリケーションの場合、これらの最適化を検討してください: ページネーション メッセージを一度にすべてではなく、バッチで読み込む 接続管理 アクティブな会話のためにのみライブクエリ接続を確立する 効率的なクエリ ターゲットを絞ったクエリを使用し、必要なフィールドのみを含める キャッシング 頻繁にアクセスされるデータのためにクライアント側キャッシングを実装する メッセージの読み込みのためのページネーションを実装する例は次のとおりです: const fetchmessages = async (conversationid, limit = 20, skip = 0) => { try { const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query descending('createdat'); query limit(limit); query skip(skip); const results = await query find(); // process results return { messages formattedmessages, hasmore results length === limit }; } catch (error) { console error('error fetching messages ', error); throw error; } }; エラーハンドリングと回復 堅牢なエラーハンドリングはリアルタイムアプリケーションにとって重要です: 接続の失敗 websocketの失敗に対する再接続ロジックを実装する メッセージの失敗 失敗したメッセージ送信を処理し、再試行オプションを提供する 状態の回復 接続の中断後にアプリケーションの状態を回復する 例えば、接続監視を追加することができます: // monitor live query connection status parse livequery on('open', () => { console log('live query connection established'); // re establish subscriptions if needed }); parse livequery on('close', () => { console log('live query connection closed'); // show connection status to user }); parse livequery on('error', (error) => { console error('live query error ', error); // handle error, potentially reconnect }); 結論 このチュートリアルでは、back4appを使用してソーシャルネットワークのための包括的なリアルタイムメッセージングシステムを構築しました。あなたは以下を実装しました ユーザー間メッセージング ユーザーが互いに見つけてメッセージを送信できるシステムを作成しました リアルタイム更新 back4appのライブクエリを使用してメッセージを即座に配信しました 入力インジケーター リアルタイムの入力通知でユーザー体験を向上させました 会話管理 複数の会話を表示および管理するためのuiを構築しました back4appのライブクエリ機能は、複雑なwebsocketサーバーやスケーリングの懸念を管理することなく、これを可能にするために必要なリアルタイムインフラストラクチャを提供しました。parse serverの組み込みサブスクリプションモデルを活用することで、応答性が高く効率的なメッセージングシステムを作成することができました。 あなたが構築したメッセージングシステムは、追加機能で強化できる堅実な基盤を提供します: 次のステップ 既読通知 メッセージの既読状況を追跡する機能を実装する グループ会話 複数の参加者をサポートするようにシステムを拡張する メディア共有 画像、動画、その他の添付ファイルをサポートする メッセージリアクション ユーザーがメッセージに絵文字で反応できるようにする メッセージ削除 メッセージを削除または編集する機能を実装する 高度な検索 会話内での検索機能を追加する back4gramソーシャルネットワークアプリケーションの完全なコード、メッセージングシステムを含む、は、 githubリポジトリ https //github com/templates back4app/back4gram をチェックできます。 back4appのデータベース、api、クラウド機能、ファイルストレージ、ユーザー管理、リアルタイム機能の組み合わせは、機能豊富なソーシャルネットワークアプリケーションを構築するための優れた選択肢です。back4appを使用することで、複雑なバックエンドインフラストラクチャの管理ではなく、優れたユーザーエクスペリエンスの創造に集中できます。