Project Templates
Social Network
如何使用React构建社交网络
113 分
介绍 在本教程中,您将构建 back4gram,一个类似于 instagram 的全功能社交网络平台。back4gram 允许用户创建帐户,分享带有图像的帖子,通过点赞和评论进行互动,搜索内容,并通过实时消息进行沟通。该项目演示了如何将 react 强大的前端功能与 back4app 的强大后端服务结合起来,以创建一个现代的、功能丰富的社交应用程序。 由于其基于组件的架构,react 是社交网络前端的完美选择,这种架构允许可重用的 ui 元素和高效的渲染。同时,back4app 提供了一个托管的 parse server 后端,处理用户身份验证、数据存储、文件上传和实时功能,而无需您从头构建复杂的服务器基础设施。 通过完成本教程,您将构建一个完整的社交网络,具有: 用户认证(注册、登录、密码重置) 个人资料管理 带图片上传的帖子创建 社交互动(点赞、评论) 带输入指示的实时消息传递 内容搜索功能 用户设置和偏好 在这个过程中,您将获得宝贵的技能: 使用钩子和上下文的react开发 使用chakra ui的ui开发 通过back4app进行parse server集成 使用livequery的实时数据管理 用户认证流程 文件上传处理 响应式设计实现 无论您是想推出自己的社交平台,还是仅仅想了解现代社交网络是如何构建的,本教程将为您提供实现目标所需的知识和实践经验。 在任何时候,您都可以访问完整的代码, github 。 先决条件 要完成本教程,您需要: 一个 back4app 账户 注册一个免费的账户, back4app com https //www back4app com/ 您将使用此工具来创建和管理您的后端服务 在您的本地机器上安装 node js 和 npm 安装 node js(版本 14 x 或更高)和 npm 来自 nodejs org https //nodejs org/ 通过运行来验证您的安装 node v 和 npm v 在您的终端中 对react的基本理解 熟悉 react 组件、钩子和 jsx 如果你需要复习 react,请查看 官方 react 文档 https //reactjs org/docs/getting started html 代码编辑器 任何现代代码编辑器,如 visual studio code、sublime text 或 atom git(可选) 用于版本控制和跟踪代码库 补充资源 back4app 文档 https //www back4app com/docs/get started/welcome parse javascript指南 https //docs parseplatform org/js/guide/ chakra ui 文档 https //chakra ui com/docs/getting started react router 文档 https //reactrouter com/en/main 步骤 1 — 设置您的 back4app 后端 在此步骤中,您将创建一个新的 back4app 项目并配置社交网络应用所需的数据库架构。back4app 提供一个托管的 parse 服务器,将处理用户身份验证、数据存储和实时功能。 创建新的 back4app 项目 登录到您的 back4app 账户并导航到仪表板。 点击 "创建新应用" 按钮。 输入 "back4gram" 作为您的应用名称,选择最近的服务器区域,然后点击 "创建"。 一旦您的应用创建完成,您将被重定向到应用仪表板。 理解数据库模式 在您的数据库中创建类之前,让我们了解我们社交网络所需的数据模型。根据我们的应用程序需求,我们将需要以下类: 用户 (在parse中默认已存在) 这个类处理用户身份验证和个人信息 我们将通过添加个人简介和头像等额外字段来扩展它 帖子 存储用户帖子,包括文本内容和图像 字段:内容(字符串),作者(指向用户的指针),图片(文件),点赞(数字),评论(数组),创建时间(日期) 评论 存储对帖子的评论 字段:内容(字符串),作者(指向用户的指针),帖子(指向帖子的指针),创建于(日期) 对话 代表用户之间的聊天对话 字段:参与者(用户的指针数组),最后消息(字符串),更新时间(日期) 消息 对话中的单个消息 字段:文本(字符串),发送者(指向用户的指针),对话(指向对话的指针),创建时间(日期) 输入状态 (用于实时输入指示) 跟踪用户在对话中输入时的情况 字段:用户(指向用户的指针),对话(指向对话的指针),正在输入(布尔值) 创建数据库类 现在,让我们在您的 back4app 数据库中创建这些类: 在您的 back4app 控制面板中导航到 "数据库" 部分。 扩展用户类 点击已经存在的"用户"类 添加以下列: 生物 (类型 字符串) 头像 (类型 文件) 关注者(类型:数字,默认:0) 以下 (类型 数字, 默认 0) 创建 post 类 点击 "创建一个班级" 输入 "post" 作为类名并选择 "创建一个空类" 添加以下列: 内容(类型:字符串) 作者(类型:指向 user 的指针) 图像(类型:文件) 喜欢 (类型 数字, 默认 0) 评论(类型:数组) 创建于 (类型 日期,自动添加) 创建评论类 点击 "创建一个班级" 输入 "comment" 作为类名并选择 "创建一个空类" 添加以下列: 内容(类型:字符串) 作者(类型:指向 user 的指针) 帖子(类型:指向帖子的指针) 创建于(类型:日期,自动添加) 创建对话类 点击 "创建一个班级" 输入 "conversation" 作为类名并选择 "创建一个空类" 添加以下列: 参与者(类型:数组) 最后消息 (类型 字符串) 更新于(类型:日期,自动添加) 创建消息类 点击 "创建一个班级" 输入 "message" 作为类名并选择 "创建一个空类" 添加以下列: 文本(类型:字符串) 发送者(类型:指向 user 的指针) 对话(类型:指向对话的指针) 创建于(类型:日期,自动添加) 创建 typingstatus 类 点击 "创建一个班级" 输入 "typingstatus" 作为类名并选择 "创建一个空类" 添加以下列: 用户(类型:指向 user 的指针) 对话(类型:指向对话的指针) 正在输入 (类型 布尔值) 设置班级权限(可选) 为了确保数据安全,我们需要为每个类别配置适当的访问控制列表(acl): 在您的 back4app 控制面板中导航到 "安全性与密钥" 部分。 在“班级级别安全”下,配置以下权限: 用户类 公开读取访问:已启用(以便用户可以查看其他用户的个人资料) 公共写入访问:禁用(用户只能修改自己的个人资料) 课后 公开阅读访问:已启用(每个人都可以查看帖子) 公共写入访问:已启用(经过身份验证的用户可以创建帖子) 添加一个 clp 以限制更新/删除仅限于作者 评论类 公开阅读访问:已启用(每个人都可以看到评论) 公共写入访问:启用(经过身份验证的用户可以创建评论) 添加一个 clp 以限制更新/删除仅限于作者 对话课 公共阅读访问:禁用(对话是私密的) 公共写入访问:启用(经过身份验证的用户可以创建对话) 添加一个clp以限制对对话参与者的读/写访问 消息类 公共读取访问:禁用(消息是私密的) 公共写入访问:已启用(经过身份验证的用户可以发送消息) 添加一个clp以限制对对话参与者的读/写访问 typingstatus 类 公共读取访问:已禁用(输入状态为私密) 公共写入访问:已启用(经过身份验证的用户可以更新输入状态) 添加一个clp以限制对对话参与者的读/写访问 设置实时功能的 livequery 要启用实时功能,如消息和输入指示器,我们需要配置 livequery: 在您的 back4app 控制面板中导航到 "服务器设置" 部分。 在 "parse server" 下,找到 "livequery" 部分并启用它。 添加以下类以便被 livequery 监控: 消息 输入状态 帖子(用于实时更新点赞和评论) 保存您的更改。 获取您的应用程序密钥 您需要您的 back4app 应用程序密钥以将 react 前端连接到后端: 导航到 "应用设置" > "安全性和密钥" 部分。 记下以下密钥: 应用程序 id javascript 密钥 服务器 url livequery 服务器 url(子域名配置,用于实时功能) 您将在 react 应用程序中使用这些密钥来初始化 parse。 步骤 2 — 创建 react 前端项目 在此步骤中,您将设置一个新的 react 项目并配置它以与您的 back4app 后端一起使用。您将安装必要的依赖项,创建项目结构,并连接到您的 parse 服务器。 设置新的 react 项目 让我们开始创建一个新的 react 应用程序,使用 create react app,它提供了一个现代的构建设置,无需配置。 打开终端并导航到您想要创建项目的目录。 运行以下命令以创建一个新的 react 应用程序: npx create react app back4gram 项目创建后,导航到项目目录: cd back4gram 启动开发服务器以确保一切正常工作: npm start 这将在您的浏览器中打开新的 react 应用程序,地址为 http //localhost 3000 http //localhost 3000 。 安装必要的依赖项 现在,让我们安装我们社交网络应用程序所需的包: 停止开发服务器(在终端中按 ctrl+c)。 安装 parse sdk 以连接到 back4app: npm install parse 安装 react router 以进行导航: npm install react router dom 安装 chakra ui 以用于我们的用户界面组件: npm install @chakra ui/react @emotion/react @emotion/styled framer motion 安装其他 ui 工具和图标库: npm install react icons 项目结构说明 让我们用清晰的结构来组织我们的项目。在 src 文件夹中创建以下目录: mkdir p src/components/ui src/pages src/contexts src/utils 每个目录的用途如下: 组件 可重用的 ui 组件 ui 基本的 ui 组件,如按钮、表单、模态框 特定功能的其他组件文件夹(例如,帖子、评论) 页面 对应于路由的完整页面组件 上下文 用于状态管理的 react 上下文提供者 工具 实用函数和助手 创建环境变量 为了安全地存储您的 back4app 凭据,请在项目根目录中创建一个 env 文件: 创建一个名为 env local 的新文件,放在项目根目录中: touch env local 打开文件并添加您的 back4app 凭据: react app parse app id=your application id react app parse js key=your javascript key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your app id back4app io 用您在步骤 1 中获得的实际 back4app 凭据替换占位符值。 确保将 env local 添加到您的 gitignore 文件中,以防止提交敏感信息。 使用 back4app 凭据配置 parse sdk 现在,让我们设置 parse sdk 以连接到您的 back4app 后端: 创建一个新文件 src/utils/parseconfig js // src/utils/parseconfig js import parse from 'parse/dist/parse min js'; // 初始化 parse parse initialize( process env react app parse app id, process env react app parse js key ); parse serverurl = process env react app parse server url; // 初始化实时查询 if (process env react app parse live query url) { parse livequeryserverurl = process env react app parse live query url; } export default parse; 更新您的 src/index js 文件以导入 parse 配置: import react from 'react'; import reactdom from 'react dom/client'; import ' /index css'; import app from ' /app'; import reportwebvitals from ' /reportwebvitals'; import ' /utils/parseconfig'; // 导入 parse 配置 const root = reactdom createroot(document getelementbyid('root')); root render( \<react strictmode> \<app /> \</react strictmode> ); reportwebvitals(); 设置应用组件与路由 让我们更新主应用组件以包含路由和chakra ui提供者: 更新 src/app js import react from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import { chakraprovider, extendtheme } from '@chakra ui/react'; // 导入页面(我们接下来会创建这些) import landingpage from ' /pages/landingpage'; import loginpage from ' /pages/loginpage'; import signuppage from ' /pages/signuppage'; import resetpasswordpage from ' /pages/resetpasswordpage'; import feedpage from ' /pages/feedpage'; import profilepage from ' /pages/profilepage'; import postdetailspage from ' /pages/postdetailspage'; import messagespage from ' /pages/messagespage'; import searchpage from ' /pages/searchpage'; import settingspage from ' /pages/settingspage'; // 创建自定义主题 const theme = extendtheme({ config { initialcolormode 'dark', usesystemcolormode false, }, colors { brand { 50 '#e5f4ff', 100 '#b8dcff', 200 '#8ac5ff', 300 '#5cadff', 400 '#2e96ff', 500 '#147dff', 600 '#0061cc', 700 '#004799', 800 '#002d66', 900 '#001433', }, }, }); function app() { return ( \<chakraprovider theme={theme}> \<router> \<routes> \<route path="/" element={\<landingpage />} /> \<route path="/login" element={\<loginpage />} /> \<route path="/signup" element={\<signuppage />} /> \<route path="/reset password" element={\<resetpasswordpage />} /> \<route path="/feed" element={\<feedpage />} /> \<route path="/profile" element={\<profilepage />} /> \<route path="/post/\ id" element={\<postdetailspage />} /> \<route path="/messages" element={\<messagespage />} /> \<route path="/search" element={\<searchpage />} /> \<route path="/settings" element={\<settingspage />} /> \</routes> \</router> \</chakraprovider> ); } export default app; 创建一个受保护的路由组件 为了保护需要身份验证的路由,让我们创建一个 protectedroute 组件: 首先,创建一个 authcontext 来管理用户认证状态: // src/contexts/authcontext js import react, { createcontext, usestate, usecontext, useeffect } from 'react'; import parse from 'parse/dist/parse min js'; const authcontext = createcontext(); export function useauth() { return usecontext(authcontext); } export function authprovider({ children }) { const \[currentuser, setcurrentuser] = usestate(null); const \[isloading, setisloading] = usestate(true); useeffect(() => { // check if user is already logged in const checkuser = async () => { try { const user = await parse user current(); setcurrentuser(user); } catch (error) { console error('error checking current user ', error); } finally { setisloading(false); } }; checkuser(); }, \[]); // login function const login = async (username, password) => { try { const user = await parse user login(username, password); setcurrentuser(user); return user; } catch (error) { throw error; } }; // signup function const signup = async (username, email, password) => { try { const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); const result = await user signup(); setcurrentuser(result); return result; } catch (error) { throw error; } }; // logout function const logout = async () => { try { await parse user logout(); setcurrentuser(null); } catch (error) { throw error; } }; // reset password function const resetpassword = async (email) => { try { await parse user requestpasswordreset(email); } catch (error) { throw error; } }; const value = { currentuser, isloading, login, signup, logout, resetpassword, }; return \<authcontext provider value={value}>{children}\</authcontext provider>; } 现在,创建 protectedroute 组件: // src/components/protectedroute js import react from 'react'; import { navigate } from 'react router dom'; import { useauth } from ' /contexts/authcontext'; import { flex, spinner } from '@chakra ui/react'; function protectedroute({ children }) { const { currentuser, isloading } = useauth(); if (isloading) { return ( \<flex justify="center" align="center" height="100vh"> \<spinner size="xl" /> \</flex> ); } if (!currentuser) { return \<navigate to="/login" />; } return children; } export default protectedroute; 更新 app 组件以使用 authprovider 和 protectedroute: // src/app js (updated) import react from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import { chakraprovider, extendtheme } from '@chakra ui/react'; import { authprovider } from ' /contexts/authcontext'; import protectedroute from ' /components/protectedroute'; // import pages import landingpage from ' /pages/landingpage'; import loginpage from ' /pages/loginpage'; import signuppage from ' /pages/signuppage'; import resetpasswordpage from ' /pages/resetpasswordpage'; import feedpage from ' /pages/feedpage'; import profilepage from ' /pages/profilepage'; import postdetailspage from ' /pages/postdetailspage'; import messagespage from ' /pages/messagespage'; import searchpage from ' /pages/searchpage'; import settingspage from ' /pages/settingspage'; // theme configuration (same as before) const theme = extendtheme({ // theme configuration }); function app() { return ( \<chakraprovider theme={theme}> \<authprovider> \<router> \<routes> \<route path="/" element={\<landingpage />} /> \<route path="/login" element={\<loginpage />} /> \<route path="/signup" element={\<signuppage />} /> \<route path="/reset password" element={\<resetpasswordpage />} /> \<route path="/feed" element={ \<protectedroute> \<feedpage /> \</protectedroute> } /> \<route path="/profile" element={ \<protectedroute> \<profilepage /> \</protectedroute> } /> \<route path="/post/\ id" element={ \<protectedroute> \<postdetailspage /> \</protectedroute> } /> \<route path="/messages" element={ \<protectedroute> \<messagespage /> \</protectedroute> } /> \<route path="/search" element={ \<protectedroute> \<searchpage /> \</protectedroute> } /> \<route path="/settings" element={ \<protectedroute> \<settingspage /> \</protectedroute> } /> \</routes> \</router> \</authprovider> \</chakraprovider> ); } export default app; 创建一个基本的登录页面 让我们创建一个简单的登录页面来测试我们的设置: // src/pages/landingpage js import react from 'react'; import { box, heading, text, button, vstack, flex } from '@chakra ui/react'; import { link as routerlink } from 'react router dom'; function landingpage() { return ( \<box bg="gray 900" minh="100vh" color="white"> \<flex direction="column" align="center" justify="center" textalign="center" py={20} \> \<heading size="2xl" mb={4}> back4gram \</heading> \<text fontsize="lg" maxw="600px" mb={8}> join a vibrant community where your voice matters share stories, ideas, and moments with friends and the world \</text> \<vstack spacing={4} maxw="md" mx="auto"> \<button as={routerlink} to="/signup" colorscheme="brand" size="lg" w="full" \> create account \</button> \<button as={routerlink} to="/login" variant="outline" size="lg" w="full" \> log in \</button> \</vstack> \</flex> \</box> ); } export default landingpage; 测试您的设置 现在您已经设置了 react 应用程序的基本结构并将其连接到 back4app,让我们进行测试: 启动开发服务器: npm start 打开您的浏览器并导航到 http //localhost 3000 http //localhost 3000 您应该看到带有注册或登录按钮的登录页面。 检查您的浏览器控制台,以确保没有与 parse 初始化相关的错误。 步骤 3 — 实现身份验证功能 在此步骤中,我们将使用 back4app 的 parse server 为我们的社交网络应用程序实现用户身份验证功能。我们将研究 parse 身份验证系统的工作原理,并实现登录、注册和密码重置功能。 理解 parse 用户身份验证系统 back4app 的 parse server 通过 parse user 类提供了全面的用户管理系统。让我们了解 parse 身份验证在我们应用程序中的工作原理: parse user 类 该 parse user 类是 parse object 的一个特殊子类,专门用于用户管理。在我们的 back4gram 应用中,我们使用它来: 存储用户凭据(用户名、电子邮件、密码) 管理身份验证状态 自动处理会话令牌 查看我们的实现,我们可以看到如何与 parse user 类进行交互: // from signuppage js const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); await user signup(); 这段代码创建一个新的 parse user 对象,设置所需字段,并调用 signup() 方法在 back4app 中注册用户。 parse中的身份验证流程 让我们来看看身份验证在我们的应用程序中是如何工作的: 注册流程 在我们的 signuppage js 中,我们收集用户名、电子邮件和密码 我们验证输入数据(检查空字段、有效的电子邮件格式、密码长度) 我们创建一个新的 parse user 对象并设置凭据 我们调用 signup(),它将数据发送到 back4app 在存储之前解析哈希密码 成功后,用户将自动使用会话令牌登录 登录流程 在我们的 loginpage js 中,我们收集用户名和密码 我们使用这些凭据调用 parse user login() 解析验证凭据与存储的数据 如果有效,parse会生成一个会话令牌 会话令牌会自动存储在浏览器存储中 会话管理 解析自动将会话令牌包含在所有api请求中 我们使用 parse user current() 来获取当前登录的用户 会话在页面刷新时保持 实现用户注册 让我们检查一下我们的signuppage组件,以了解用户注册是如何实现的: 表单验证 在将数据发送到 back4app 之前,我们验证用户输入: // from signuppage js const validateform = () => { const newerrors = {}; if (!username trim()) { newerrors username = 'username is required'; } if (!email trim()) { newerrors email = 'email is required'; } else if (!/\s+@\s+\\ \s+/ test(email)) { newerrors email = 'email is invalid'; } if (!password) { newerrors password = 'password is required'; } else if (password length < 6) { newerrors password = 'password must be at least 6 characters'; } if (password !== confirmpassword) { newerrors confirmpassword = 'passwords do not match'; } seterrors(newerrors); return object keys(newerrors) length === 0; }; 此验证确保: 用户名不能为空 电子邮件有效 密码至少为 6 个字符 密码和确认匹配 处理注册错误 我们的注册处理程序包括对 parse 特定错误的错误处理: // from signuppage js try { // create a new user const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); await user signup(); toaster create({ title 'success', description 'account created successfully!', type 'success', }); navigate('/feed'); } catch (error) { toaster create({ title 'signup failed', description error message, type 'error', }); // handle specific parse errors if (error code === 202) { seterrors({ errors, username 'username already taken'}); } else if (error code === 203) { seterrors({ errors, email 'email already in use'}); } } back4app 返回特定的错误代码,我们可以用来为用户提供有用的反馈: 代码 202:用户名已被占用 代码 203:电子邮件已在使用中 用户注册/注册的完整代码可以在这里找到。 实现用户登录 我们的 loginpage 组件使用 parse user login() 处理用户身份验证: 登录表单 登录表单收集用户名和密码: // from loginpage js \<form onsubmit={handlelogin}> \<vstack spacing={4}> \<field label="username"> \<input type="text" value={username} onchange={(e) => setusername(e target value)} placeholder="your username" required /> \</field> \<field label="password" errortext={error} \> \<input type="password" value={password} onchange={(e) => setpassword(e target value)} placeholder="your password" required /> \</field> \<link as={routerlink} to="/reset password" alignself="flex end" fontsize="sm"> forgot password? \</link> \<button colorscheme="blue" width="full" type="submit" loading={isloading} \> log in \</button> \</vstack> \</form> 会话验证 如前所示,我们在登录页面加载时检查是否存在会话: // from loginpage js useeffect(() => { const checkcurrentuser = async () => { try { const user = await parse user current(); if (user) { setcurrentuser(user); navigate('/feed'); } } catch (error) { console error('error checking current user ', error); } }; checkcurrentuser(); }, \[navigate]); 这是 parse 的一个关键特性 它自动管理浏览器存储中的会话令牌,使我们能够轻松检查用户是否已登录。 实现密码重置 back4app 提供了内置的密码重置流程。在我们的应用程序中,我们从登录表单链接到密码重置页面: // from loginpage js \<link as={routerlink} to="/reset password" alignself="flex end" fontsize="sm"> forgot password? \</link> back4app 中的密码重置过程如下: 用户请求使用他们的电子邮件重置密码 parse 向用户的电子邮件发送一个特殊的重置链接 用户点击链接并设置新密码 parse 在数据库中更新密码哈希 要在我们的应用程序中实现这一点,我们将使用: // example password reset implementation try { await parse user requestpasswordreset(email); // show success message } catch (error) { // handle error } 为经过身份验证的用户保护路由 为了保护我们应用程序中的某些路由,我们使用一个 protectedroute 组件来检查用户是否经过身份验证: // from protectedroute js function protectedroute({ children }) { const { currentuser, isloading } = useauth(); if (isloading) { return ( \<flex justify="center" align="center" height="100vh"> \<spinner size="xl" /> \</flex> ); } if (!currentuser) { return \<navigate to="/login" />; } return children; } 这个组件: 使用我们的 authcontext 检查用户是否已登录 在检查时显示加载旋转器 如果未找到用户,则重定向到登录页面 如果用户已认证,则渲染受保护的内容 我们在路由设置中使用这个组件: // from app js \<route path="/feed" element={ \<protectedroute> \<feedpage /> \</protectedroute> } /> back4app 认证配置 back4app 在仪表板中提供了多个认证配置选项: 电子邮件验证 您可以要求用户在登录之前进行电子邮件验证 在 "服务器设置" > "parse 服务器" > "用户认证" 中配置此项 密码策略 设置最低密码长度和复杂性要求 在 "服务器设置" > "parse 服务器" > "用户认证" 中配置此项 会话时长 控制用户会话保持有效的时间 在 "服务器设置" > "parse 服务器" > "会话配置" 中配置此项 电子邮件模板 自定义验证和密码重置电子邮件 在 "应用设置" > "电子邮件模板" 中配置此项 测试您的身份验证实现 确保您的身份验证系统正常工作: 测试用户注册 尝试使用有效凭据注册 尝试使用已存在的用户名注册(应该显示错误) 检查用户是否出现在您的 back4app 仪表板的 " user" 类下 测试用户登录 尝试使用正确的凭据登录(应该重定向到动态信息) 尝试使用错误的凭据登录(应该显示错误) 测试会话持久性 登录并刷新页面(应该保持登录状态) 关闭并重新打开浏览器(如果会话有效,应该保持登录状态) 测试受保护的路由 尝试在未登录时访问 /feed(应该重定向到登录) 尝试在登录时访问 /feed(应该显示动态信息页面) 登录组件的代码可以在这里找到。 步骤 4 — 开发动态功能 在这一步中,您将实现核心社交网络功能:动态。这是用户创建帖子、查看其他人内容并通过点赞和评论进行互动的地方。我们将使用 back4app 的 parse server 来存储和检索帖子,处理图像的文件上传,并实现实时更新。 理解信息流页面结构 我们应用中的信息流页面有三个主要组件: 用于导航的侧边栏 主要的信息流区域,包含帖子创建和帖子列表 一个趋势部分(在较大屏幕上) 让我们看看这是如何在我们的 feedpage js 中实现的: // from feedpage js main structure function feedpage() { // state and hooks return ( \<flex minh="100vh" bg="gray 800" color="white"> {/ left sidebar (navigation) /} \<box w={\['0px', '200px']} bg="gray 900" p={4} display={\['none', 'block']}> {/ navigation links /} \</box> {/ main content (feed) /} \<box flex="1" p={4} overflowy="auto"> {/ post creation form /} {/ posts list /} \</box> {/ right sidebar (trending) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']}> {/ trending content /} \</box> \</flex> ); } 这个响应式布局适应不同的屏幕尺寸,在移动设备上隐藏侧边栏。 在 back4app 中创建帖子类 在实现前端之前,让我们确保我们的 back4app 数据库为帖子正确设置: post类应具有以下字段: 内容 (字符串):帖子的文本内容 图片 (文件):可选的图片附件 作者 (指向用户):创建帖子的用户 点赞 (数字):帖子的点赞数量 被谁点赞 (数组):点赞帖子的用户id数组 创建时间 (日期):由parse自动添加 为post类设置适当的权限: 公开读取访问:每个人都应该能够阅读帖子 公开写入访问:经过身份验证的用户应该能够创建帖子 更新/删除权限:只有作者应该能够修改他们的帖子 实现帖子创建 让我们来看看在我们的 feedpage 组件中如何实现帖子创建: // from feedpage js post creation state const \[postcontent, setpostcontent] = usestate(''); const \[postimage, setpostimage] = usestate(null); const \[imagepreview, setimagepreview] = usestate(''); const \[issubmitting, setissubmitting] = usestate(false); // image selection handler const handleimageselect = (e) => { if (e target files && e target files\[0]) { const file = e target files\[0]; setpostimage(file); // create preview url const previewurl = url createobjecturl(file); setimagepreview(previewurl); } }; // post submission handler const handlesubmitpost = async () => { if (!postcontent trim() && !postimage) { toaster create({ title 'error', description 'please add some text or an image to your post', type 'error', }); return; } setissubmitting(true); try { // create a new post object const post = parse object extend('post'); const newpost = new post(); // set post content newpost set('content', postcontent); newpost set('author', parse user current()); newpost set('likes', 0); newpost set('likedby', \[]); // if there's an image, upload it if (postimage) { const parsefile = new parse file(postimage name, postimage); await parsefile save(); newpost set('image', parsefile); } // save the post await newpost save(); // reset form setpostcontent(''); setpostimage(null); setimagepreview(''); // refresh posts fetchposts(); toaster create({ title 'success', description 'your post has been published!', type 'success', }); } catch (error) { toaster create({ title 'error', description error message, type 'error', }); } finally { setissubmitting(false); } }; 关于帖子创建的关键点: parse中的文件处理 parse file用于将图像上传到back4app的存储 文件首先被保存,然后附加到帖子对象上 back4app自动处理文件存储并生成url 创建parse对象 我们扩展'post'类,使用 parse object extend('post') 我们使用 new post() 我们使用 set() 方法设置属性 我们使用 save() 用户关联 我们使用 parse user current() 这在数据库中创建了一个指针关系 帖子创建表单的用户界面如下: {/ post creation form /} \<box mb={6} bg="gray 700" p={4} borderradius="md"> \<vstack align="stretch" spacing={4}> \<textarea placeholder="what's on your mind?" value={postcontent} onchange={(e) => setpostcontent(e target value)} minh="100px" /> {imagepreview && ( \<box position="relative"> \<image src={imagepreview} maxh="200px" borderradius="md" /> \<iconbutton icon={\<closeicon />} size="sm" position="absolute" top="2" right="2" onclick={() => { setpostimage(null); setimagepreview(''); }} /> \</box> )} \<hstack> \<button lefticon={\<attachmenticon />} onclick={() => document getelementbyid('image upload') click()} \> add image \</button> \<input id="image upload" type="file" accept="image/ " onchange={handleimageselect} display="none" /> \<button colorscheme="blue" ml="auto" isloading={issubmitting} onclick={handlesubmitpost} disabled={(!postcontent trim() && !postimage) || issubmitting} \> post \</button> \</hstack> \</vstack> \</box> 获取和显示帖子 现在让我们看看如何从 back4app 获取和显示帖子: // from feedpage js fetching posts const \[posts, setposts] = usestate(\[]); const \[isloading, setisloading] = usestate(true); const \[page, setpage] = usestate(0); const \[hasmore, sethasmore] = usestate(true); const postsperpage = 10; const fetchposts = async (loadmore = false) => { try { const currentpage = loadmore ? page + 1 0; // create a query for the post class const post = parse object extend('post'); const query = new parse query(post); // include the author object (pointer) query include('author'); // sort by creation date, newest first query descending('createdat'); // pagination query limit(postsperpage); query skip(currentpage postsperpage); // execute the query const results = await query find(); // process the results const fetchedposts = results map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, likedby post get('likedby') || \[], createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); // update state if (loadmore) { setposts(prevposts => \[ prevposts, fetchedposts]); setpage(currentpage); } else { setposts(fetchedposts); setpage(0); } // check if there are more posts to load sethasmore(results length === postsperpage); } catch (error) { console error('error fetching posts ', error); toaster create({ title 'error', description 'failed to load posts please try again ', type 'error', }); } finally { setisloading(false); } }; // load posts when component mounts useeffect(() => { fetchposts(); }, \[]); 关于获取帖子的重要点: 解析查询 我们创建一个查询,使用 new parse query(post) 我们包含相关对象,使用 query include('author') 我们使用 query descending('createdat') 我们使用分页,使用 query limit() 和 query skip() 我们执行查询,使用 query find() 处理结果 parse 对象有一个 get() 方法来访问属性 对于文件字段,我们使用 file url() 来获取 url 我们将 parse 对象转换为普通的 javascript 对象,以便用于 react 状态 分页 我们实现了 "加载更多" 功能,使用页面跟踪 在进行额外请求之前,我们检查是否还有更多帖子可加载 帖子以列表形式显示: {/ posts list /} {isloading ? ( \<center py={10}> \<spinner size="xl" /> \</center> ) posts length > 0 ? ( \<vstack spacing={4} align="stretch"> {posts map(post => ( \<box key={post id} p={4} bg="gray 700" borderradius="md"> {/ post header with author info /} \<hstack mb={2}> \<avatar root size="sm"> \<avatar fallback name={post author username} /> \<avatar image src={post author avatar} /> \</avatar root> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(post createdat)}\</text> \</hstack> {/ post content /} \<text mb={4}>{post content}\</text> {/ post image if any /} {post image && ( \<image src={post image} maxh="400px" borderradius="md" mb={4} /> )} {/ post actions /} \<hstack> \<button variant="ghost" lefticon={\<likeicon />} onclick={() => handlelikepost(post id, post likedby)} color={post likedby includes(currentuser id) ? "blue 400" "white"} \> {post likes} likes \</button> \<button variant="ghost" lefticon={\<commenticon />} as={routerlink} to={`/post/${post id}`} \> comments \</button> \</hstack> \</box> ))} {/ load more button /} {hasmore && ( \<button onclick={() => fetchposts(true)} isloading={isloadingmore}> load more \</button> )} \</vstack> ) ( \<center py={10}> \<text>no posts yet be the first to post!\</text> \</center> )} 实现点赞功能 让我们来看看点赞功能是如何实现的: // from feedpage js like functionality const handlelikepost = async (postid, likedby) => { try { const currentuserid = parse user current() id; const isliked = likedby includes(currentuserid); // get the post object const post = parse object extend('post'); const query = new parse query(post); const post = await query get(postid); // update likes count and likedby array if (isliked) { // unlike remove user from likedby and decrement likes post set('likedby', likedby filter(id => id !== currentuserid)); post set('likes', (post get('likes') || 1) 1); } else { // like add user to likedby and increment likes post set('likedby', \[ likedby, currentuserid]); post set('likes', (post get('likes') || 0) + 1); } // save the updated post await post save(); // update local state setposts(prevposts => prevposts map(p => p id === postid ? { p, likes isliked ? p likes 1 p likes + 1, likedby isliked ? p likedby filter(id => id !== currentuserid) \[ p likedby, currentuserid] } p ) ); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'failed to like post please try again ', type 'error', }); } }; 关于点赞功能的关键点: 乐观更新 在服务器确认更改之前,我们立即更新用户界面 这使得应用程序感觉更具响应性 解析对象更新 我们使用 query get(postid) 我们使用 post set() 我们使用 post save() 跟踪点赞 我们同时维护一个计数( 点赞 )和一个用户列表( likedby ) 这使我们能够显示准确的计数并确定当前用户是否已点赞某个帖子 使用实时更新和livequery(可选) 当创建新帖子时,为了使信息流实时更新,我们可以使用 parse livequery: // from feedpage js livequery setup const livequerysubscription = useref(null); useeffect(() => { // set up livequery for real time updates const setuplivequery = async () => { try { const post = parse object extend('post'); const query = new parse query(post); // subscribe to new posts livequerysubscription current = await query subscribe(); // when a new post is created livequerysubscription current on('create', (post) => { // only add to feed if it's not already there setposts(prevposts => { if (prevposts some(p => p id === post id)) return prevposts; const newpost = { id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, likedby post get('likedby') || \[], createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } }; return \[newpost, prevposts]; }); }); // when a post is updated (e g , liked) livequerysubscription current on('update', (post) => { setposts(prevposts => prevposts map(p => p id === post id ? { p, content post get('content'), image post get('image') ? post get('image') url() p image, likes post get('likes') || 0, likedby post get('likedby') || \[] } p ) ); }); } catch (error) { console error('error setting up livequery ', error); } }; setuplivequery(); // clean up subscription when component unmounts return () => { if (livequerysubscription current) { livequerysubscription current unsubscribe(); } }; }, \[]); 关于 livequery 的关键点: 订阅设置 我们创建一个查询并通过 query subscribe() 这建立了与 back4app 的 livequery 服务器的 websocket 连接 事件处理 我们监听 'create' 事件,当新帖子被创建时 我们监听 'update' 事件,当帖子被修改时 我们相应地更新我们的本地状态 清理 当组件卸载时,我们取消订阅以防止内存泄漏 通过分页优化帖子加载 我们已经实现了带有 "加载更多" 按钮的基本分页。让我们通过无限滚动来增强它: // from feedpage js infinite scrolling const \[isloadingmore, setisloadingmore] = usestate(false); const feedref = useref(null); // intersection observer for infinite scrolling useeffect(() => { if (!hasmore) return; const observer = new intersectionobserver( (entries) => { if (entries\[0] isintersecting && !isloading && !isloadingmore) { loadmoreposts(); } }, { threshold 0 5 } ); const loadmoretrigger = document getelementbyid('load more trigger'); if (loadmoretrigger) { observer observe(loadmoretrigger); } return () => { if (loadmoretrigger) { observer unobserve(loadmoretrigger); } }; }, \[hasmore, isloading, isloadingmore]); const loadmoreposts = async () => { if (!hasmore || isloadingmore) return; setisloadingmore(true); try { await fetchposts(true); } finally { setisloadingmore(false); } }; 并在帖子列表的末尾添加这个: {/ infinite scroll trigger /} {hasmore && ( \<box id="load more trigger" h="20px" /> )} 关于无限滚动的关键点: 交叉观察者 我们使用交叉观察者api来检测用户何时滚动到底部 当触发元素变得可见时,我们加载更多帖子 加载状态 我们跟踪初始加载和“加载更多”的单独加载状态 这可以防止多个同时请求 性能考虑 我们一次只加载固定数量的帖子(分页) 在发出额外请求之前,我们检查是否还有更多帖子可加载 back4app 性能优化 在使用 back4app 时优化性能: 使用索引 在您的 back4app 控制面板中为经常查询的字段添加索引 对于 post 类,添加 'createdat' 和 'author' 的索引 选择性查询 使用 query select() 仅获取您需要的字段 这减少了数据传输并提高了性能 计数优化 与其获取所有帖子进行计数,不如使用 query count() 这对于确定总计数更有效 第 6 步 — 添加社交互动 在这一步中,我们将通过实现关键的社交互动功能来增强我们的社交网络:对帖子进行评论、用户个人资料和用户设置。我们将重点关注这些功能如何与 back4app 后端交互以及使其工作的机制。 在帖子上实现评论 评论是一个基本的社交互动功能,需要在 back4app 中进行适当的数据建模。让我们来看看我们的应用程序如何与 parse server 交互以实现评论: back4app 的评论数据模型 在 back4app 中,评论作为一个独立的类实现,并与用户和帖子之间建立关系: 评论类结构 内容 (字符串):评论的文本内容 作者 (指向用户):指向创建评论的用户 帖子 (指向帖子):指向被评论的帖子 创建时间 (日期):由 parse 自动管理 关系类型 用户 → 评论:一对多(一个用户可以创建多个评论) 帖子 → 评论:一对多(一个帖子可以有多个评论) 从 back4app 获取评论 我们的 postdetailspage 使用 parse 查询来获取特定帖子的评论: // from postdetailspage js comment fetching const fetchcomments = async () => { try { // create a query on the comment class const comment = parse object extend('comment'); const query = new parse query(comment); // find comments for this specific post using a pointer equality constraint query equalto('post', postobject); // include the author information query include('author'); // sort by creation date (newest first) query descending('createdat'); // execute the query const results = await query find(); // transform parse objects to plain objects for react state const commentslist = results map(comment => ({ id comment id, content comment get('content'), createdat comment get('createdat'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null } })); setcomments(commentslist); } catch (error) { console error('error fetching comments ', error); toaster create({ title 'error', description 'failed to load comments', type 'error', }); } }; back4app 的关键机制: parse object extend() 在 back4app 中创建对 comment 类的引用 query equalto() 创建约束以仅查找特定帖子的评论 query include() 执行类似连接的操作以在单个查询中获取相关对象 query descending() 按特定字段对结果进行排序 在 back4app 中创建评论 当用户添加评论时,我们创建一个新的 parse 对象并建立关系: // from postdetailspage js adding a comment const handleaddcomment = async (e) => { e preventdefault(); if (!newcomment trim()) { return; } setiscommenting(true); try { // create a new comment object in back4app const comment = parse object extend('comment'); const comment = new comment(); // set comment data and relationships comment set('content', newcomment); comment set('author', parse user current()); // pointer to current user comment set('post', postobject); // pointer to current post // save the comment to back4app await comment save(); // clear the input setnewcomment(''); // refresh comments fetchcomments(); toaster create({ title 'success', description 'your comment has been added', type 'success', }); } catch (error) { toaster create({ title 'error', description error message, type 'error', }); } finally { setiscommenting(false); } }; back4app 的关键机制: new comment() 创建 comment 类的新实例 comment set() 设置 parse 对象上的属性,包括指向相关对象的指针 comment save() 将对象发送到 back4app 进行存储 parse user current() 获取当前已认证的用户以建立作者关系 back4app 评论安全 为了正确保护 back4app 中的评论: 配置类级权限 (clps): 读取:公开(每个人都可以阅读评论) 写入:仅限认证用户(仅登录用户可以评论) 更新/删除:仅限创建者(仅评论作者可以修改或删除) 在您的 back4app 控制面板中设置这些权限: { "find" { " " true }, "get" { " " true }, "create" { " " true }, "update" { "requiresauthentication" true }, "delete" { "requiresauthentication" true }, "addfield" { "requiresauthentication" true } } 第7步 使用back4app构建用户档案 我们应用中的用户档案利用parse的内置用户类和自定义字段。让我们看看profilepage js如何与back4app交互: back4app用户类扩展 parse用户类扩展了我们社交网络的附加字段: 自定义用户字段 头像 (文件):存储在back4app文件存储中的个人资料图片 个人简介 (字符串):用户传记 网站 (字符串):用户的网站url 显示名称 (字符串):用户的显示名称 获取用户数据和帖子 我们的个人资料页面获取用户数据和用户的帖子: // from profilepage js profile data fetching const fetchuserdata = async () => { try { // get current user from parse session const currentuser = await parse user current(); if (!currentuser) { navigate('/login'); return; } setuser(currentuser); // create a query to find posts by this user const post = parse object extend('post'); const query = new parse query(post); query equalto('author', currentuser); query include('author'); query descending('createdat'); const results = await query find(); // transform parse objects to plain objects const postslist = results map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); setposts(postslist); setstats(prevstats => ({ prevstats, posts postslist length })); } catch (error) { console error('error fetching user data ', error); toaster create({ title 'error', description 'failed to load profile data', type 'error', }); } finally { setisloading(false); } }; back4app 的关键机制: parse user current() 从会话令牌中检索经过身份验证的用户 query equalto('author', currentuser) 创建一个指针相等约束以查找当前用户的帖子 post get('image') url() 访问存储在 back4app 中的 parse 文件对象的 url 实现用户设置 设置页面允许用户更新他们的个人资料信息并管理账户设置。让我们来看看它是如何与 back4app 交互的: // from settingspage js user settings implementation function settingspage() { const \[privacysettings, setprivacysettings] = usestate({ profilevisibility 'public', postprivacy 'friends' }); const \[twofactorauth, settwofactorauth] = usestate(false); const \[isopen, setisopen] = usestate(false); const cancelref = useref(); // save user settings to back4app const savesettings = async (settingstype, settingsdata) => { try { const currentuser = await parse user current(); if (!currentuser) { toaster create({ title 'error', description 'you must be logged in to save settings', type 'error', }); return; } // update the appropriate settings based on type switch (settingstype) { case 'privacy' currentuser set('privacysettings', settingsdata); break; case 'security' currentuser set('securitysettings', settingsdata); break; case 'notifications' currentuser set('notificationsettings', settingsdata); break; default break; } // save the user object await currentuser save(); toaster create({ title 'success', description 'your settings have been saved', type 'success', }); } catch (error) { toaster create({ title 'error', description error message, type 'error', }); } }; return ( \<box maxw="800px" mx="auto" p={4}> \<heading mb={6}>account settings\</heading> \<tabs root defaultvalue="profile"> \<tabs list> \<tabs trigger value="profile">profile\</tabs trigger> \<tabs trigger value="privacy">privacy\</tabs trigger> \<tabs trigger value="security">security\</tabs trigger> \<tabs trigger value="notifications">notifications\</tabs trigger> \<tabs indicator /> \</tabs list> {/ settings tabs content /} {/ /} \</tabs root> {/ account deactivation dialog /} \<dialog root open={isopen} onopenchange={setisopen}> {/ /} \</dialog root> \</box> ); } back4app 关键机制: parse user current() 获取当前用户以更新其设置 currentuser set() 更新 parse 用户对象中的用户属性 currentuser save() 将更改持久化到 back4app back4app 用户设置架构 在 back4app 中实现设置: 将这些字段添加到用户类: 隐私设置 (对象):包含隐私偏好的 json 对象 安全设置 (对象):包含安全设置的 json 对象 通知设置 (对象):包含通知偏好的 json 对象 这些对象的示例架构: // 隐私设置 { "profilevisibility" "public", // 或 "friends" 或 "private" "postprivacy" "friends", // 或 "public" 或 "private" "showactivity" true } // 安全设置 { "twofactorauth" false, "loginalerts" true } // 通知设置 { "likes" true, "comments" true, "follows" true, "messages" true } back4app 社交互动的云函数 对于更复杂的社交互动,您可以在 back4app 中实现云函数。例如,跟踪评论通知: // example cloud function for comment notifications parse cloud aftersave("comment", async (request) => { // only run for new comments, not updates if (request original) return; const comment = request object; const post = comment get("post"); const commenter = request user; // skip if user is commenting on their own post const postquery = new parse query("post"); const fullpost = await postquery get(post id, { usemasterkey true }); const postauthor = fullpost get("author"); if (postauthor id === commenter id) return; // create a notification const notification = parse object extend("notification"); const notification = new notification(); notification set("type", "comment"); notification set("fromuser", commenter); notification set("touser", postauthor); notification set("post", post); notification set("read", false); await notification save(null, { usemasterkey true }); }); 要实现这一点: 前往您的 back4app 控制面板 导航到 "云代码" > "云函数" 使用上述代码创建一个新函数 部署该函数 第 8 步 — 构建实时消息传递 在此步骤中,我们将使用 back4app 的 livequery 功能实现实时消息传递功能。这将允许用户即时交换消息,而无需刷新页面,创建类似于流行消息平台的动态聊天体验。 理解 back4app livequery 在深入实现之前,让我们了解 back4app 的 livequery 是如何工作的: 什么是实时查询? livequery 是 parse server 的一个功能,允许客户端订阅查询 当匹配这些查询的对象发生变化时,服务器会自动将更新推送给订阅的客户端 这实现了实时功能,而无需自己实现复杂的websocket处理 实时查询是如何工作的 livequery 在客户端和服务器之间建立 websocket 连接 客户订阅他们想要监控的特定查询 当与这些查询匹配的数据发生变化时,服务器通过websocket发送事件 客户端接收这些事件并相应地更新用户界面 实时查询事件 创建 当创建与查询匹配的新对象时触发 更新 当与查询匹配的现有对象被更新时触发 输入 当一个对象开始匹配查询时触发 离开 当一个对象不再匹配查询时触发 删除 当与查询匹配的对象被删除时触发 在 back4app 中设置 livequery 要为您的应用程序启用实时查询,请按照以下步骤操作: 启用您的 back4app 子域名 登录到您的 back4app 账户 导航到 "应用设置" > "服务器设置" 找到 "服务器 url 和实时查询" 块并点击 "设置" 检查 "激活您的 back4app 子域名" 选项 此子域将作为您的实时查询服务器 激活实时查询 在同一设置页面,检查“激活实时查询”选项 选择您想要通过 livequery 监控的课程: 消息(用于聊天消息) 输入状态(用于输入指示) 对话(用于对话更新) 保存您的更改 注意您的 livequery 服务器 url 您的 livequery 服务器 url 将采用以下格式: wss\ //yourappname back4app io 您需要此 url 来在您的 react 应用程序中初始化 livequery 客户端 在您的 react 应用中配置 livequery 要在您的 react 应用中使用 livequery,您需要初始化一个 livequery 客户端: // from parseconfig js or app js parse initialize( process env react app parse app id, process env react app parse js key ); parse serverurl = process env react app parse server url; // initialize live queries with your subdomain parse livequeryserverurl = process env react app parse live query url; // e g , 'wss\ //yourappname back4app io' 在你的 env local 文件中,请确保包含: react app parse live query url=wss\ //yourappname back4app io 创建消息的数据模型 我们的消息系统在back4app中需要两个主要类: 对话课程 参与者 (数组):对话中用户的用户指针数组 最后消息 (字符串):最近消息的内容 最后消息日期 (日期):最近消息的时间戳 更新时间 (日期):由parse自动管理 消息类别 对话 (指针):指向此消息所属的对话 发送者 (指针):指向发送消息的用户 内容 (字符串):消息的文本内容 阅读 (布尔值):消息是否已被阅读 创建于 (日期):由parse自动管理 输入状态类 对话 (指针):指向对话 用户 (指针):指向正在输入的用户 正在输入 (布尔值) 用户当前是否正在输入 实现消息接口 让我们来看看我们的 messagespage 如何实现实时消息传递: // from messagespage js component structure function messagespage() { const \[conversations, setconversations] = usestate(\[]); const \[selectedconversation, setselectedconversation] = usestate(null); const \[messages, setmessages] = usestate(\[]); const \[newmessage, setnewmessage] = usestate(''); const \[isloading, setisloading] = usestate(true); const \[typingusers, settypingusers] = usestate(\[]); const messagesendref = useref(null); const messagesubscription = useref(null); const typingsubscription = useref(null); const conversationsubscription = useref(null); const typingtimeoutref = useref(null); // rest of the component } 该组件维护多个状态: conversations 用户的对话列表 selectedconversation 当前选定的对话 messages 选定对话中的消息 typingusers 当前在对话中输入的用户 它还使用 refs 来存储 livequery 订阅并管理输入指示器。 订阅 livequery 以获取消息 实时消息传递的关键是订阅当前对话中的消息的livequery: // from messagespage js livequery subscription for messages const subscribetomessages = async (conversation) => { try { // unsubscribe from previous subscription if exists if (messagesubscription current) { messagesubscription current unsubscribe(); } // create a query for messages in this conversation const message = parse object extend('message'); const query = new parse query(message); // create a pointer to the conversation const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversation id; // find messages for this conversation query equalto('conversation', conversationpointer); // include the sender information query include('sender'); // subscribe to the query const subscription = await query subscribe(); messagesubscription current = subscription; // when a new message is created subscription on('create', (message) => { // add the new message to our state setmessages(prevmessages => { // check if message already exists in our list const exists = prevmessages some(m => m id === message id); if (exists) return prevmessages; // add the new message return \[ prevmessages, { id message id, content message get('content'), createdat message get('createdat'), sender { id message get('sender') id, username message get('sender') get('username'), avatar message get('sender') get('avatar') ? message get('sender') get('avatar') url() null }, read message get('read') }]; }); // scroll to bottom when new message arrives scrolltobottom(); // mark message as read if from other user if (message get('sender') id !== parse user current() id) { markmessageasread(message); } }); } catch (error) { console error('error subscribing to messages ', error); } }; 关键的 livequery 机制: 创建查询 我们为当前对话中的消息创建一个查询 订阅查询 我们调用 query subscribe() 来开始监听更改 处理事件 我们使用 subscription on('create', callback) 来处理新消息 取消订阅 我们存储订阅引用,并在需要时取消订阅 使用 livequery 实现输入指示器 输入指示器是使用 livequery 实现的另一个实时功能: // from messagespage js livequery for typing indicators const subscribetotypingstatus = async (conversation) => { try { // unsubscribe from previous subscription if exists if (typingsubscription current) { typingsubscription current unsubscribe(); } // create a query for typing status in this conversation const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); // create a pointer to the conversation const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversation id; // find typing status for this conversation query equalto('conversation', conversationpointer); // include the user information query include('user'); // subscribe to the query const subscription = await query subscribe(); typingsubscription current = subscription; // when a typing status is updated subscription on('update', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); // update typing users list settypingusers(prevtypingusers => { // if user is typing, add them to the list if (istyping) { // check if user is already in the list const exists = prevtypingusers some(u => u id === user id); if (exists) return prevtypingusers; // add user to typing list return \[ prevtypingusers, { id user id, username user get('username') }]; } else { // if user stopped typing, remove them from the list return prevtypingusers filter(u => u id !== user id); } }); }); // when a new typing status is created subscription on('create', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); // only add to typing users if they are actually typing if (istyping && user id !== parse user current() id) { settypingusers(prevtypingusers => { // check if user is already in the list const exists = prevtypingusers some(u => u id === user id); if (exists) return prevtypingusers; // add user to typing list return \[ prevtypingusers, { id user id, username user get('username') }]; }); } }); } catch (error) { console error('error subscribing to typing status ', error); } }; 更新输入状态 当用户输入时,我们更新他们的输入状态: // from messagespage js updating typing status const updatetypingstatus = async (istyping) => { if (!selectedconversation) return; try { const currentuser = await parse user current(); // create a pointer to the conversation const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = selectedconversation id; // check if a typing status already exists const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); query equalto('conversation', conversationpointer); query equalto('user', currentuser); let typingstatus = await query first(); if (typingstatus) { // update existing typing status typingstatus set('istyping', istyping); } else { // create new typing status typingstatus = new typingstatus(); typingstatus set('conversation', conversationpointer); typingstatus set('user', currentuser); typingstatus set('istyping', istyping); } await typingstatus save(); } catch (error) { console error('error updating typing status ', error); } }; // handle typing indicator with debounce const handletyping = () => { updatetypingstatus(true); // clear previous timeout if (typingtimeoutref current) { cleartimeout(typingtimeoutref current); } // set typing to false after 3 seconds of inactivity typingtimeoutref current = settimeout(() => { updatetypingstatus(false); }, 3000); }; 发送消息 在发送消息时,我们创建一个新的消息对象,并让livequery处理更新: // from messagespage js sending messages const sendmessage = async (e) => { e preventdefault(); if (!newmessage trim() || !selectedconversation) return; try { const currentuser = await parse user current(); // create a pointer to the conversation const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = selectedconversation id; // create a new message const message = parse object extend('message'); const message = new message(); message set('content', newmessage); message set('sender', currentuser); message set('conversation', conversationpointer); message set('read', false); await message save(); // update the conversation with the last message const conversation = await new parse query(conversation) get(selectedconversation id); conversation set('lastmessage', newmessage); conversation set('lastmessagedate', new date()); await conversation save(); // clear the input setnewmessage(''); // set typing status to false updatetypingstatus(false); } catch (error) { console error('error sending message ', error); toast({ title 'error', description 'failed to send message', status 'error', duration 3000, isclosable true, }); } }; 清理 livequery 订阅 当 livequery 订阅不再需要时,清理它们是很重要的: // from messagespage js cleanup useeffect(() => { // fetch initial conversations fetchconversations(); subscribetoconversations(); // cleanup subscriptions when component unmounts return () => { if (messagesubscription current) { messagesubscription current unsubscribe(); } if (typingsubscription current) { typingsubscription current unsubscribe(); } if (conversationsubscription current) { conversationsubscription current unsubscribe(); } if (typingtimeoutref current) { cleartimeout(typingtimeoutref current); } }; }, \[]); back4app livequery 性能考虑 在实现 livequery 时,请考虑以下性能提示: 具体查询 仅订阅所需的数据 使用约束来限制订阅的范围 例如,仅订阅当前对话中的消息 谨慎管理订阅 当不再需要数据时取消订阅 当上下文变化时创建新的订阅 存储订阅引用以便稍后取消订阅 使用acl进行安全性 在消息和对话对象上设置适当的acl 确保用户只能访问他们参与的对话 livequery尊重acl,因此未授权用户将无法接收更新 优化livequery服务器 在back4app仪表板中,配置需要livequery的类 不要为不需要实时更新的类启用livequery 第9步 — 实现搜索功能 在这一步中,我们将为我们的社交网络实现全面的搜索功能。用户将能够搜索其他用户、按内容搜索帖子和标签。这个功能将使用户更容易发现内容并与平台上的其他人连接。 理解 back4app 中的搜索 在深入实现之前,让我们了解一下 back4app 中的搜索是如何工作的: 解析查询系统 back4app 使用 parse server 的查询系统进行搜索 可以跨多个类执行查询 您可以通过精确匹配、包含、以 开头等方式进行搜索。 文本搜索选项 以 开头 查找以特定字符串开头的字符串 包含 查找包含特定子字符串的字符串 匹配 使用正则表达式进行更复杂的模式匹配 全文 (企业功能)提供高级全文搜索功能 性能考虑 文本搜索可能会消耗大量资源 应为经常搜索的字段创建索引 查询应优化以限制结果数量 构建搜索页面 我们的 searchpage 组件将处理不同类型的搜索并显示结果。让我们检查一下它的结构: // from searchpage js component structure function searchpage() { const \[searchquery, setsearchquery] = usestate(''); const \[searchtype, setsearchtype] = usestate('users'); // 'users', 'posts', 'hashtags' const \[searchresults, setsearchresults] = usestate(\[]); const \[isloading, setisloading] = usestate(false); const \[trendingtopics, settrendingtopics] = usestate(\[]); // rest of the component } 该组件维护的状态包括: 用户输入的搜索查询 正在执行的搜索类型 搜索结果 加载状态 热门话题 实现用户搜索 让我们看看如何在 back4app 中搜索用户: // from searchpage js user search implementation const searchusers = async (query) => { setisloading(true); try { // create a query on the user class const userquery = new parse query(parse user); // search for usernames that contain the query string (case insensitive) userquery matches('username', new regexp(query, 'i')); // limit results to improve performance userquery limit(20); const users = await userquery find(); // transform parse objects to plain objects const userresults = users map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null, bio user get('bio') || '' })); setsearchresults(userresults); } catch (error) { console error('error searching users ', error); toaster create({ title 'error', description 'failed to search users', type 'error', }); } finally { setisloading(false); } }; back4app 的关键机制: new parse query(parse user) 在用户类上创建查询 userquery matches('username', new regexp(query, 'i')) 对用户名执行不区分大小写的正则匹配 userquery limit(20) 限制结果以提高性能 userquery find() 执行查询并返回匹配的用户 实现帖子内容搜索 现在让我们看看如何通过内容搜索帖子: // from searchpage js post search implementation const searchposts = async (query) => { setisloading(true); try { // create a query on the post class const post = parse object extend('post'); const postquery = new parse query(post); // search for posts with content containing the query string postquery matches('content', new regexp(query, 'i')); // include the author information postquery include('author'); // sort by creation date (newest first) postquery descending('createdat'); // limit results postquery limit(20); const posts = await postquery find(); // transform parse objects to plain objects const postresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); setsearchresults(postresults); } catch (error) { console error('error searching posts ', error); toaster create({ title 'error', description 'failed to search posts', type 'error', }); } finally { setisloading(false); } }; back4app 的关键机制: parse object extend('post') 引用 post 类 postquery matches('content', new regexp(query, 'i')) 对帖子内容执行不区分大小写的正则匹配 postquery include('author') 在单个查询中包含作者信息 postquery descending('createdat') 按创建日期排序结果 实现标签搜索 标签搜索需要不同的方法。我们将搜索包含标签的帖子: // from searchpage js hashtag search implementation const searchhashtags = async (query) => { setisloading(true); try { // remove # if present at the beginning const hashtagquery = query startswith('#') ? query substring(1) query; // create a query on the post class const post = parse object extend('post'); const postquery = new parse query(post); // search for posts with content containing the hashtag // we use word boundaries to find actual hashtags postquery matches('content', new regexp(`#${hashtagquery}\\\b`, 'i')); // include the author information postquery include('author'); // sort by creation date (newest first) postquery descending('createdat'); // limit results postquery limit(20); const posts = await postquery find(); // transform parse objects to plain objects const hashtagresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); setsearchresults(hashtagresults); } catch (error) { console error('error searching hashtags ', error); toaster create({ title 'error', description 'failed to search hashtags', type 'error', }); } finally { setisloading(false); } }; back4app的关键机制: 我们使用带有单词边界的正则表达式( \\\b )来查找实际的标签 这种方法查找内容包含特定标签的帖子 实现热门话题 要实现热门话题,我们需要分析最近的帖子并计算标签出现的次数: // from searchpage js fetching trending topics const fetchtrendingtopics = async () => { try { // create a query on the post class const post = parse object extend('post'); const query = new parse query(post); // get posts from the last 7 days const oneweekago = new date(); oneweekago setdate(oneweekago getdate() 7); query greaterthan('createdat', oneweekago); // limit to a reasonable number for analysis query limit(500); const posts = await query find(); // extract hashtags from post content const hashtagcounts = {}; posts foreach(post => { const content = post get('content') || ''; // find all hashtags in the content const hashtags = content match(/#(\w+)/g) || \[]; // count occurrences of each hashtag hashtags foreach(hashtag => { const tag = hashtag tolowercase(); hashtagcounts\[tag] = (hashtagcounts\[tag] || 0) + 1; }); }); // convert to array and sort by count const trendingarray = object entries(hashtagcounts) map((\[hashtag, count]) => ({ hashtag, count })) sort((a, b) => b count a count) slice(0, 10); // get top 10 settrendingtopics(trendingarray); } catch (error) { console error('error fetching trending topics ', error); } }; back4app的关键机制: 我们查询过去7天的帖子,使用 query greaterthan('createdat', oneweekago) 我们分析内容以提取和计数标签 我们按频率排序以找到最受欢迎的标签 处理搜索执行 现在让我们看看如何根据搜索类型处理搜索执行: // from searchpage js search execution const handlesearch = (e) => { e preventdefault(); if (!searchquery trim()) return; switch (searchtype) { case 'users' searchusers(searchquery); break; case 'posts' searchposts(searchquery); break; case 'hashtags' searchhashtags(searchquery); break; default searchusers(searchquery); } }; 在back4app中优化搜索 要优化back4app中的搜索性能: 创建索引 导航到您的 back4app 控制面板 转到 "数据库浏览器" > 选择类(例如,用户,帖子) 点击 "索引" 标签 为经常搜索的字段创建索引: 对于用户类:创建一个索引在 用户名 对于帖子类:创建一个索引在 内容 使用查询约束 始终使用 limit() 来限制结果数量 使用 select() 仅获取所需字段 使用 skip() 进行分页处理大结果集 考虑使用云函数进行复杂搜索 对于更复杂的搜索逻辑,实现一个云函数 这使您能够进行服务器端处理并返回优化的结果 高级搜索的示例云函数: // example cloud function for advanced search parse cloud define("advancedsearch", async (request) => { const { query, type, limit = 20 } = request params; if (!query) { throw new error("search query is required"); } let results = \[]; switch (type) { case 'users' const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); results = await userquery find({ usemasterkey true }); break; case 'posts' const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery limit(limit); results = await postquery find({ usemasterkey true }); break; // add more search types as needed default throw new error("invalid search type"); } return results; }); 第10步 — 测试和部署您的社交网络 在最后一步中,我们将介绍如何测试您的应用程序,准备它以便于生产,将其部署到托管服务,并监控和扩展您的 back4app 后端。这些步骤对于确保您的社交网络在生产环境中顺利运行至关重要。 测试您的应用程序 在部署之前,彻底测试您的应用程序以捕捉任何错误或问题是很重要的: 1\ 手动测试 创建一个测试计划,涵盖您应用程序的所有关键功能: 用户认证 使用有效和无效输入进行测试注册 使用正确和错误的凭据进行测试登录 测试密码重置功能 测试会话持久性和注销 帖子功能 测试创建带有文本和图像的帖子 测试在动态中查看帖子 测试对帖子进行点赞和评论 测试删除帖子 社交互动 测试查看用户资料 测试对帖子进行评论 测试实时消息传递 搜索功能 测试搜索用户 测试搜索帖子 测试标签搜索 跨浏览器测试 在chrome、firefox、safari和edge上测试 在移动浏览器上的测试 2\ 自动化测试 为了更强大的测试,实施自动化测试: // example jest test for the login component import react from 'react'; import { render, fireevent, waitfor } from '@testing library/react'; import loginpage from ' /src/pages/loginpage'; import parse from 'parse/dist/parse min js'; // mock parse jest mock('parse/dist/parse min js', () => ({ user { login jest fn() } })); test('login form submits with username and password', async () => { parse user login mockresolvedvalueonce({ id '123', get () => 'testuser' }); const { getbylabeltext, getbyrole } = render(\<loginpage />); // fill in the form fireevent change(getbylabeltext(/username/i), { target { value 'testuser' } }); fireevent change(getbylabeltext(/password/i), { target { value 'password123' } }); // submit the form fireevent click(getbyrole('button', { name /log in/i })); // check if parse user login was called with correct arguments await waitfor(() => { expect(parse user login) tohavebeencalledwith('testuser', 'password123'); }); }); 3\ back4app 测试 测试您的 back4app 配置: 云函数 使用各种输入测试所有云函数 安全性 验证您的类级权限是否正常工作 实时查询 使用多个客户端测试实时功能 准备生产环境 在部署之前,优化您的应用程序以适应生产环境: 1\ 环境配置 为开发和生产创建单独的环境文件: \# env development react app parse app id=your dev app id react app parse js key=your dev js key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your dev app back4app io \# env production react app parse app id=your production app id react app parse js key=your production js key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your prod app back4app io 2\ 构建优化 优化您的 react 构建: // in your package json "scripts" { "analyze" "source map explorer 'build/static/js/ js'", "build" "generate sourcemap=false react scripts build" } 安装 source map explorer 以分析您的包大小: npm install save dev source map explorer 3\ 性能优化 实现代码分割以减少初始加载时间: // in app js, use react lazy for route components import react, { suspense, lazy } from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import { chakraprovider } from '@chakra ui/react'; import loadingspinner from ' /components/loadingspinner'; // lazy load pages const landingpage = lazy(() => import(' /pages/landingpage')); const loginpage = lazy(() => import(' /pages/loginpage')); const feedpage = lazy(() => import(' /pages/feedpage')); // other pages function app() { return ( \<chakraprovider> \<router> \<suspense fallback={\<loadingspinner />}> \<routes> \<route path="/" element={\<landingpage />} /> \<route path="/login" element={\<loginpage />} /> \<route path="/feed" element={\<feedpage />} /> {/ other routes /} \</routes> \</suspense> \</router> \</chakraprovider> ); } 部署到托管服务 有几种选项可以部署您的 react 应用程序: 1\ 部署到 vercel vercel 是 react 应用程序的一个很好的选择: 安装 vercel cli: npm install g vercel 部署您的应用程序: vercel 用于生产部署: vercel prod 2\ 部署到 netlify netlify是另一个优秀的选择: 安装netlify cli: npm install g netlify cli 构建你的应用程序: npm run build 部署到netlify: netlify deploy 用于生产部署: netlify deploy prod 3\ 部署到github pages 对于一个简单的部署选项: 安装 gh pages: npm install save dev gh pages 添加到 package json: "homepage" "https //yourusername github io/your repo name", "scripts" { "predeploy" "npm run build", "deploy" "gh pages d build" } 部署: npm run deploy 监控和扩展您的 back4app 后端 随着您的社交网络的增长,您需要监控和扩展您的 back4app 后端: 1\ 监控性能 back4app 提供了多种工具来监控您的应用程序: 仪表板分析 监控api请求、存储使用情况和文件操作 日志 检查服务器日志以查找错误和性能问题 性能指标 跟踪响应时间并识别瓶颈 要访问这些工具: 前往您的back4app仪表板 导航到“分析”以获取使用统计信息 检查“日志”以获取详细操作日志 2\ 扩展您的后端 当您的用户基础增长时,您可能需要扩展您的back4app后端: 升级您的计划 转到更高层级的计划以获得更多资源 优化查询 使用索引并限制查询以提高性能 实现缓存 对频繁访问的数据使用客户端缓存 3\ 数据库优化 优化您的数据库以获得更好的性能: 创建索引 为经常查询的字段添加索引 // 示例:在 user 类中创建 'username' 字段的索引 const schema = new parse schema(' user'); schema addindex('username index', { username 1 }); schema update(); 使用聚合管道 用于复杂的数据操作 // 示例:按用户计数帖子 const pipeline = \[ { group { objectid '$author', count { $sum 1 } } } ]; const results = await parse cloud aggregate('post', pipeline); 4\ 为媒体实施cdn 为了更快的图像和媒体传输: 配置像cloudflare或amazon cloudfront这样的cdn 更新您的back4app文件存储设置以使用cdn 更新您应用程序中的文件url以使用cdn域名 5\ 设置监控警报 设置警报以便在出现问题时通知您: 前往您的 back4app 控制面板 导航到 "应用设置" > "警报" 配置警报以监控: 高 api 使用率 错误率激增 数据库大小限制 服务器停机 已完成工作的总结 在本教程中,您已经: 建立一个强大的后端基础设施 创建了一个 back4app 账户并配置了您的应用程序 为用户、帖子、评论和消息设计了一个数据库架构 配置的安全设置和类级权限 设置实时功能的livequery 构建了一个现代的react前端 使用chakra ui组件创建了一个响应式用户界面 使用 react router 实现了客户端路由 开发了可重用的组件用于帖子、评论和用户个人资料 使用 parse javascript sdk 将您的前端连接到 back4app 实现了核心社交网络功能 用户认证(注册,登录,密码重置) 帖子创建和互动(点赞,评论) 用户个人资料和设置 用户之间的实时消息传递 用户、帖子和标签的搜索功能 为生产优化 实现了性能优化,如代码分割 为开发和生产设置环境配置 学习如何将您的应用程序部署到托管服务 探索了您的 back4app 后端的监控和扩展策略 您现在拥有一个社交网络应用的坚实基础,可以扩展和定制以满足您的特定需求。 扩展应用程序的下一步 以下是一些增强您的社交网络应用程序的激动人心的方法: 高级媒体功能 添加对视频上传和播放的支持 实现图像滤镜和编辑工具 创建故事或短暂内容功能 添加对gif和其他丰富媒体的支持 增强的社交互动 实现一个朋友推荐引擎 添加群组或社区功能 创建具有rsvp功能的活动 为所有用户交互开发通知系统 货币化选项 实施高级会员功能 为数字商品添加应用内购买 创建一个用户对用户交易的市场 与支付处理器如stripe集成 移动体验 将您的应用程序转换为渐进式网络应用程序 (pwa) 使用 react native 开发原生移动应用 为移动设备实现推送通知 优化用户界面以适应不同的屏幕尺寸和方向 分析与洞察 集成分析工具以跟踪用户参与度 创建内容表现的仪表板 为新功能实施a/b测试 开发用户行为洞察以改善平台 内容审核 实施自动内容过滤 创建不当内容的报告系统 开发内容审核的管理工具 使用机器学习进行智能内容分析 学习的额外资源 为了继续扩展您的知识和技能,这里有一些有价值的资源: back4app 文档和教程 back4app 文档 https //www back4app com/docs/get started/welcome parse javascript指南 https //docs parseplatform org/js/guide/ back4app youtube频道 https //www youtube com/c/back4app react 和现代 javascript react 文档 https //reactjs org/docs/getting started html javascript info https //javascript info/ egghead io react 课程 https //egghead io/q/react 用户界面和用户体验设计 chakra ui 文档 https //chakra ui com/docs/getting started 用户界面设计模式 https //ui patterns com/ 尼尔森诺曼集团用户体验研究 https //www nngroup com/articles/ 性能优化 web dev 性能 https //web dev/performance scoring/ react 性能优化 https //reactjs org/docs/optimizing performance html 谷歌页面速度洞察 https //developers google com/speed/pagespeed/insights/ 社区与支持 堆栈溢出 https //stackoverflow\ com/questions/tagged/parse platform parse社区论坛 https //community parseplatform org/ react开发社区 https //dev to/t/react 请记住,建立一个成功的社交网络是一个迭代的过程。首先要有一个坚实的基础(你现在已经有了),收集用户反馈,并根据实际使用模式不断改进你的应用程序。 我们希望这个教程能为您提供知识和信心,以使用 react 和 back4app 构建出色的应用程序。祝您编码愉快