Project Templates
Social Network
实时聊天为您的社交网络
49 分
介绍 在本教程中,您将学习如何使用 back4app 为您的社交网络应用程序实现实时消息系统。您将构建一个完整的聊天功能,允许用户即时发送和接收消息,查看输入指示器,并管理对话——这些都是吸引用户的社交平台的基本功能。 back4app 是一个基于 parse server 的后端即服务(baas)平台,通过其实时查询功能提供强大的实时能力。借助 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 hooks 和组件生命周期。 步骤 1 – 理解 back4app 的实时功能 在我们开始编码之前,让我们了解一下 back4app 如何通过其实时查询功能实现实时功能。 实时查询解释 实时查询是 parse server 的一项功能,允许客户端订阅查询,并在与这些查询匹配的对象被创建、更新或删除时接收更新。这非常适合构建实时应用程序,如聊天系统。 实时查询的工作原理如下: 客户端订阅特定查询(例如,“对话 x 中的所有消息”) 当服务器上创建、更新或删除匹配的对象时 实时查询会自动通知所有订阅的客户端 客户端可以根据这些事件更新其用户界面 在消息系统的上下文中,这意味着: 当发送新消息时,所有参与该对话的用户会立即收到消息 当用户开始输入时,其他用户可以实时看到输入指示器 当消息被阅读时,可以为所有参与者更新已读回执 在 back4app 上设置实时查询 要使用实时查询,您需要在 back4app 控制面板中启用它: 登录到您的 back4app 控制面板 导航到服务器设置 > 网络托管和实时查询 启用实时查询 添加您希望与实时查询一起使用的类(在我们的案例中为 "message" 和 "typingstatus") 接下来,在您的客户端代码中,您需要初始化 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 中创建这些类: 对话类 对话类将具有以下字段: 参与者 (用户的指针数组):参与对话的用户 最后消息 (字符串):最近消息的文本 更新时间 (日期):当记录更改时由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); } }; 这个函数: 查询对话类,获取当前用户是参与者的对话 格式化结果以在用户界面中显示 如果可用,将第一个对话设置为活动对话 back4gram 项目: 找到 这里 完整代码,适用于 社交网络示例项目 ,使用 back4app 构建。 步骤 6 – 处理消息和实时更新 现在让我们实现获取消息和设置实时更新的功能,使用实时查询: 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', }); } }; 现在,让我们实现实时消息更新的实时查询订阅: 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', }); } }; 这个实时查询设置: 创建对当前对话中新消息的订阅 实时处理传入消息 在新消息到达时更新用户界面 用最新消息和时间戳更新对话列表 管理重复消息(当通过实时查询同时发送和接收时可能会发生) 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 数据库中创建并保存新消息 更新对话的最后一条消息和时间戳 处理通过实时查询返回消息时的重复预防 向发送者提供即时反馈 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 函数允许用户通过用户名查找其他用户。它查询back4app中的用户类,排除当前用户的结果,并格式化数据以供显示。 对话创建 该 startconversation 函数处理查找现有对话和创建新对话。它: 检查用户之间是否已经存在对话 如果存在,则加载该对话及其消息 如果不存在对话,则创建一个新的对话 为新消息和输入状态设置实时查询订阅 通过实现这些功能,用户可以轻松找到其他用户并与他们开始对话,这对于社交网络消息系统至关重要。 back4gram项目: 查找 这里 完整代码, 社交网络示例项目 使用back4app构建 第10步 – 构建聊天用户界面 现在我们已经实现了所有功能,让我们为我们的消息系统创建用户界面。用户界面将包括: 一个列出所有对话的侧边栏 一个搜索界面用于查找用户 一个显示消息的聊天窗口 一个带有输入指示器支持的消息输入框 这是我们 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> ); 让我们将用户界面分解为其主要组件: 对话侧边栏 左侧边栏显示所有对话的列表 每个对话显示其他用户的姓名、头像和最后一条消息 用户可以点击任何对话以查看并继续聊天 顶部的搜索栏允许查找其他用户以开始新的对话 消息显示 主要区域显示活动对话的消息 当前用户的消息出现在右侧,背景为蓝色 其他用户的消息出现在左侧,背景为灰色 每条消息显示消息文本和发送时间 当新消息到达时,消息列表会自动滚动到底部 消息输入 底部是一个输入区域,用于输入和发送消息 用户可以按回车键发送消息 发送按钮也可以用来发送消息 在输入区域输入时会自动触发输入指示器 在选择对话之前,输入功能是禁用的 输入指示器 当其他用户正在输入时,会出现“正在输入 ”的指示器 该指示器通过 typingstatus 实时查询订阅进行实时更新 该用户界面提供了一个干净直观的消息界面,类似于用户对流行消息应用程序的期望。 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(); }, \[]); 这个测试函数: 尝试创建对消息类的订阅 为连接事件设置事件处理程序 记录每个步骤的结果 在10秒后自动取消订阅 通过观察控制台日志,您可以验证: 实时查询 url 已正确配置 可以建立连接 事件被正确接收 如果您在实时查询连接中遇到任何问题,请检查以下内容: 您的应用程序已启用 back4app 实时查询 实时查询 url 在您的 parse 初始化中已正确设置 消息类已添加到 back4app 的实时查询类列表中 您的网络允许 websocket 连接 back4gram 项目: 查找 这里 完整代码, 社交网络示例项目 使用 back4app 构建 步骤 12 – 理解 back4app 如何实现实时消息传递 现在我们已经构建了一个完整的消息系统,让我们更深入地了解 back4app 的功能如何使实时消息传递成为可能。 实时查询基础设施 back4app 的实时查询功能建立在 websockets 之上,它提供了客户端和服务器之间的持久连接。这与传统的 http 请求/响应模型根本不同: 传统 rest api 客户端发出请求 → 服务器响应 → 连接关闭 websockets/实时查询 客户端建立持久连接 → 服务器可以随时推送更新 这种持久连接使我们的消息系统具备实时功能。当创建新消息时,back4app 会自动将该更新推送给所有订阅的客户端,而无需它们轮询服务器。 parse server的订阅模型 parse server中的订阅模型是基于查询的。这意味着: 您订阅特定的查询(例如,“对话x中的所有消息”) 您仅接收与该查询匹配的对象的更新 您可以接收不同类型的事件( 创建 , 更新 , 删除 , 等等) 这种基于查询的方法非常强大,因为它允许精确的订阅。在我们的消息系统中,我们只订阅活动对话的消息,这比订阅所有消息要高效得多。 数据库考虑事项 使用back4app的实时查询时,有一些重要的数据库考虑事项: 索引 确保在订阅查询中使用的字段已建立索引,以提高性能 数据大小 保持对象小以减少负载大小并提高性能 扩展 实时查询连接使用资源,因此在扩展应用程序时要考虑这一点 为了获得最佳性能,请考虑: 在消息类中创建 对话 字段的索引 使用指针而不是嵌入大型对象 管理订阅的生命周期以避免内存泄漏 安全考虑 在实现实时消息传递时,安全性至关重要: acls 和 clps 使用访问控制列表和类级权限来保护您的数据 连接认证 只有经过认证的用户才能建立实时查询连接 数据验证 使用云代码在服务器端验证消息内容 速率限制 实施速率限制以防止滥用 例如,您可能希望使用云代码在保存之前验证和清理消息: // 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 更新 检查输入指示器、消息列表和会话列表是否正确更新 多用户测试 要正确测试消息系统,您需要与多个用户进行测试: 打开两个浏览器窗口或标签 以不同用户登录每个窗口 在它们之间开始对话 来回发送消息 测试输入指示器和实时更新 性能优化 对于较大的应用程序,请考虑以下优化: 分页 分批加载消息,而不是一次性加载所有消息 连接管理 仅为活跃对话建立实时查询连接 高效查询 使用有针对性的查询,仅包含必要字段 缓存 为频繁访问的数据实现客户端缓存 以下是实现消息加载分页的示例: 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的实时查询即时传递消息 输入指示器 通过实时输入通知增强用户体验 会话管理 构建了一个用于查看和管理多个会话的用户界面 back4app的实时查询功能提供了实现这一目标所需的实时基础设施,而无需管理复杂的websocket服务器或扩展问题。通过利用parse server内置的订阅模型,您能够创建一个响应迅速且高效的消息系统。 您构建的消息系统提供了一个坚实的基础,您可以通过附加功能来增强它: 下一步 已读回执 实现消息已读状态跟踪 群组对话 扩展系统以支持多个参与者 媒体共享 添加对图像、视频和其他附件的支持 消息反应 允许用户用表情符号对消息进行反应 消息删除 实现删除或编辑消息的功能 高级搜索 在对话中添加搜索功能 要查看 back4gram 社交网络应用程序的完整代码,包括其消息系统,您可以查看 github 仓库 https //github com/templates back4app/back4gram back4app 的数据库、api、云函数、文件存储、用户管理和实时功能的结合,使其成为构建功能丰富的社交网络应用程序的绝佳选择。通过使用 back4app,您可以专注于创建出色的用户体验,而不是管理复杂的后端基础设施。