Project Templates
Social Network
Чат в реальном времени для вашей социальной сети
53 мин
введение в этом учебном пособии вы узнаете, как реализовать систему обмена сообщениями в реальном времени для вашего приложения социальной сети с использованием back4app вы создадите полноценный функционал чата, который позволяет пользователям мгновенно отправлять и получать сообщения, видеть индикаторы ввода и управлять беседами важные функции для вовлекающих социальных платформ back4app это платформа backend as a service (baas), построенная на parse server, которая предоставляет мощные возможности в реальном времени через свою функцию live query с инфраструктурой реального времени back4app вы можете создавать отзывчивые системы обмена сообщениями, не управляя сложными серверами websocket или проблемами масштабирования к концу этого учебного пособия вы создадите полностью функциональную систему обмена сообщениями, аналогичную той, что используется в back4gram, приложении социальной сети вы реализуете создание бесед, обмен сообщениями в реальном времени, индикаторы ввода и поиск пользователей, предоставляя вашим пользователям бесшовный опыт общения проект back4gram найдите здесь полный код для образца проекта социальной сети созданного с помощью back4app предварительные требования чтобы завершить этот учебник, вам потребуется учетная запись back4app вы можете зарегистрироваться для получения бесплатной учетной записи на back4app com https //www back4app com проект back4app, настроенный с инициализированным parse javascript sdk установленный 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 автоматически уведомляет всех подписанных клиентов клиенты могут затем обновить свой интерфейс в ответ на эти события в контексте системы обмена сообщениями это означает когда новое сообщение отправляется, все пользователи в этом разговоре получают его мгновенно когда пользователь начинает печатать, другие пользователи могут видеть индикатор набора текста в реальном времени когда сообщение прочитано, уведомления о прочтении могут быть обновлены для всех участников настройка live query на back4app чтобы использовать live query, вам нужно включить его в вашей панели управления back4app войдите в свою панель управления back4app перейдите в настройки сервера > веб хостинг и live query включите live query добавьте классы, которые вы хотите использовать с live query (в нашем случае, "сообщение" и "статус набора текста") далее, в вашем клиентском коде вам нужно инициализировать клиент 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 разговор представляет собой чат между двумя или более пользователями сообщение представляет собой отдельные сообщения в рамках разговора статус набора текста отслеживает, когда пользователи набирают текст в разговоре давайте создадим эти классы в back4app класс разговор класс conversation будет иметь следующие поля участники (массив указателей на пользователя) пользователи, участвующие в разговоре последнеесообщение (строка) текст самого последнего сообщения обновлено (дата) автоматически обновляется parse, когда запись изменяется класс сообщение класс сообщение будет иметь разговор (указатель на разговор) разговор, к которому принадлежит это сообщение отправитель (указатель на пользователя) пользователь, который отправил сообщение текст (строка) содержимое сообщения создано (дата) автоматически создается parse, когда сообщение отправляется класс typingstatus класс typingstatus будет отслеживать индикаторы набора текста пользователь (указатель на пользователя) пользователь, который печатает беседа (указатель на беседу) беседа, в которой происходит набор текста печатает (булевый) указывает, печатает ли пользователь в данный момент теперь давайте настроим структуру нашего проекта для реализации этой системы обмена сообщениями проект 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 для разговоров, где текущий пользователь является участником форматирует результаты для отображения в пользовательском интерфейсе устанавливает первый разговор как активный, если он доступен проект 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', }); } }; эта настройка live query создает подписку на новые сообщения в текущем разговоре обрабатывает входящие сообщения в реальном времени обновляет интерфейс с новыми сообщениями по мере их поступления обновляет список разговоров с последним сообщением и временной меткой управляет дублирующимися сообщениями (которые могут возникать при отправке и получении через 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); } }; эта реализация подписывается на обновления статуса набора текста от других пользователей в разговоре обновляет интерфейс, когда кто то другой набирает текст отправляет обновления статуса набора текста, когда текущий пользователь набирает текст проект 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', }); } }; этот код предоставляет две важные функции для нашей системы обмена сообщениями поиск пользователей функция searchusers позволяет пользователям находить других пользователей по имени пользователя она запрашивает класс user в back4app, исключает текущего пользователя из результатов и форматирует данные для отображения создание беседы функция startconversation обрабатывает как поиск существующих бесед, так и создание новых она проверяет, существует ли уже беседа между пользователями если она существует, загружает эту беседу и её сообщения если беседы не существует, создаёт новую настраивает подписки на live query для новых сообщений и статуса набора текста с этими реализованными функциями пользователи могут легко находить других пользователей и начинать с ними беседы, что является важным для системы обмена сообщениями в социальной сети проект back4gram найдите здесь полный код для образца проекта социальной сети созданного с помощью back4app шаг 10 – создание пользовательского интерфейса чата теперь, когда мы реализовали всю функциональность, давайте создадим пользовательский интерфейс для нашей системы обмена сообщениями интерфейс будет состоять из боковая панель со списком всех разговоров интерфейс поиска для нахождения пользователей чат окно, показывающее сообщения поле ввода сообщения с поддержкой индикатора набора вот jsx для нашего компонента messagespage 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> ); давайте разберем пользовательский интерфейс на его основные компоненты боковая панель разговоров левая боковая панель показывает список всех разговоров каждый разговор показывает имя другого пользователя, аватар и последнее сообщение пользователи могут нажимать на любой разговор, чтобы просмотреть и продолжить чат поле поиска вверху позволяет находить других пользователей для начала новых разговоров отображение сообщений основная область отображает сообщения для активного разговора сообщения от текущего пользователя появляются справа с синим фоном сообщения от других пользователей появляются слева с серым фоном каждое сообщение показывает текст сообщения и время его отправки список сообщений автоматически прокручивается вниз, когда приходят новые сообщения ввод сообщения внизу находится область ввода для набора и отправки сообщений пользователи могут нажимать enter для отправки сообщений кнопка отправить также позволяет отправлять сообщения набор текста в области ввода автоматически вызывает индикаторы набора текста ввод отключен, пока не выбран разговор индикаторы ввода когда другой пользователь печатает, появляется индикатор "печатает " индикатор обновляется в реальном времени с использованием подписки на live query typingstatus этот интерфейс предоставляет чистый и интуитивно понятный интерфейс для обмена сообщениями, аналогичный тому, что пользователи ожидают от популярных приложений для обмена сообщениями проект back4gram найдите здесь полный код для образца проекта социальной сети созданного с помощью back4app шаг 11 – тестирование соединений live query чтобы убедиться, что наши соединения live query работают правильно, давайте добавим тестовую функцию, которая проверяет соединение при монтировании компонента // 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 основана на websockets, которые обеспечивают постоянное соединение между клиентом и сервером это принципиально отличается от традиционной модели http запроса/ответа традиционный rest api клиент отправляет запрос → сервер отвечает → соединение закрывается websockets/живые запросы клиент устанавливает постоянное соединение → сервер может отправлять обновления в любое время это постоянное соединение и позволяет реализовать возможности реального времени в нашей системе обмена сообщениями когда создается новое сообщение, back4app автоматически отправляет это обновление всем подписанным клиентам, не требуя от них опроса сервера модель подписки parse server модель подписки в parse server основана на запросах это означает вы подписываетесь на конкретный запрос (например, "все сообщения в разговоре x") вы получаете обновления только для объектов, которые соответствуют этому запросу вы можете получать разные типы событий ( создание , обновление , удаление , и т д ) этот подход, основанный на запросах, чрезвычайно мощный, потому что он позволяет делать точные подписки в нашей системе обмена сообщениями мы подписываемся только на сообщения для активного разговора, что гораздо эффективнее, чем подписка на все сообщения соображения по базе данных при использовании live query с back4app есть несколько важных соображений по базе данных индексация убедитесь, что поля, используемые в запросах подписки, индексированы для повышения производительности размер данных держите объекты маленькими, чтобы уменьшить размер полезной нагрузки и улучшить производительность масштабирование соединения live query используют ресурсы, поэтому учитывайте это при масштабировании вашего приложения для оптимальной производительности учитывайте создание индексов на полях разговора в классе message использование указателей вместо встраивания больших объектов управление жизненным циклом подписок, чтобы избежать утечек памяти соображения безопасности при реализации обмена сообщениями в реальном времени безопасность имеет решающее значение acls и clp используйте списки управления доступом и разрешения на уровне класса для защиты ваших данных аутентификация соединения только аутентифицированные пользователи должны иметь возможность устанавливать соединения live query валидация данных проверяйте содержимое сообщений на стороне сервера с помощью 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 – тестирование и оптимизация системы обмена сообщениями чтобы убедиться, что ваша система обмена сообщениями работает правильно, вам следует тщательно ее протестировать тестирование базовой функциональности отправка сообщений убедитесь, что сообщения могут быть отправлены и получены создание беседы проверьте создание новых бесед поиск пользователей подтвердите, что пользователи могут быть найдены и беседы начаты обновления интерфейса проверьте, что индикаторы ввода, списки сообщений и списки бесед обновляются правильно тестирование с несколькими пользователями чтобы правильно протестировать систему обмена сообщениями, вам нужно протестировать с несколькими пользователями откройте два окна или вкладки браузера войдите как разные пользователи в каждом начните разговор между ними отправляйте сообщения взад и вперед проверьте индикаторы ввода и обновления в реальном времени оптимизация производительности для более крупных приложений рассмотрите эти оптимизации пагинация загружайте сообщения партиями, а не все сразу управление подключениями устанавливайте соединения live query только для активных разговоров эффективные запросы используйте целевые запросы и включайте только необходимые поля кэширование реализуйте кэширование на стороне клиента для часто запрашиваемых данных вот пример реализации пагинации для загрузки сообщений 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 вы реализовали обмен сообщениями между пользователями создана система, в которой пользователи могут находить и обмениваться сообщениями друг с другом обновления в реальном времени использован live query от back4app для мгновенной доставки сообщений индикаторы ввода улучшен пользовательский опыт с помощью уведомлений о вводе в реальном времени управление беседами создан пользовательский интерфейс для просмотра и управления несколькими беседами функция live query от back4app предоставила необходимую инфраструктуру в реальном времени, чтобы сделать это возможным без необходимости управлять сложными серверами websocket или проблемами масштабирования используя встроенную модель подписки parse server, вы смогли создать отзывчивую и эффективную систему обмена сообщениями система обмена сообщениями, которую вы создали, предоставляет надежную основу, которую вы можете улучшить дополнительными функциями следующие шаги чтение сообщений реализовать отслеживание статуса прочтения сообщений групповые беседы расширить систему для поддержки нескольких участников обмен медиафайлами добавить поддержку изображений, видео и других вложений реакции на сообщения позволить пользователям реагировать на сообщения с помощью эмодзи удаление сообщений реализовать возможность удаления или редактирования сообщений расширенный поиск добавить возможности поиска в беседах для полного кода приложения социальной сети back4gram, включая его систему обмена сообщениями, вы можете ознакомиться с репозиторием на github https //github com/templates back4app/back4gram комбинация базы данных, api, облачных функций, хранения файлов, управления пользователями и возможностей реального времени от back4app делает его отличным выбором для создания функционально насыщенных приложений социальной сети используя back4app, вы можете сосредоточиться на создании отличного пользовательского опыта, а не на управлении сложной инфраструктурой на стороне сервера