Project Templates
Social Network
如何为社交网络构建后端?
47 分
介绍 在本指南中,您将发现如何使用一个 后端即服务平台 来设置一个强大的社交网络后端。 这个后端将被设计用于处理用户身份验证、数据管理、实时通信等,同时确保安全性和效率。 在本教程结束时,您的项目将如下所示: 社交网络概述 提示:要获取完整的源代码,请访问我们的 github 仓库 github https //github com/templates back4app/back4gram 。 关键要点 简化的后端管理: 利用 back4app 的托管后端来简化用户身份验证、数据存储和实时功能。 实时互动: 利用 livequery 实现动态消息传递和即时内容更新。 全面的用户和内容处理: 建立强大的用户资料、帖子、评论和社交互动系统。 可扩展性和性能: 应用查询优化、缓存和批处理操作,以保持高性能,随着网络的增长。 高级集成: 通过云函数、后台作业和推送通知增强您的后端,以提供更丰富的用户体验。 先决条件 在开始之前,请确保您拥有: 一个 back4app 账户 在 back4app com https //www back4app com/ node js 和 npm 从 nodejs org https //nodejs org/ 安装 node js (v14 x 或更高版本) 基本的 javascript 和 react 知识 一个代码编辑器 使用 visual studio code 或 sublime text 等编辑器 步骤 1 — 配置您的 back4app 后端 在 back4app 上创建新项目 登录到您的 back4app 控制面板。 点击 "创建一个新应用" 为您的应用命名(例如,"back4social"),并选择离您最近的服务器区域。 一旦项目创建完成,您将在您的 back4app 控制面板中看到它列出。这个项目将是本教程中讨论的所有后端配置的基础。 设计数据库架构 您的社交网络后端将使用以下类: 用户 (默认由parse提供) 扩展字段,如个人简介和头像 帖子 包含文本内容和图片上传 评论 存储用户对帖子的评论 对话 表示用户之间的聊天会话 消息 包含对话中的单个消息 输入状态 指示用户正在输入时 添加数据库类 在back4app中,数据存储在 类 中。您可以在back4app仪表板中或通过ai代理创建新类。 要使用仪表板创建,请: 导航到“数据库”部分 在您的 back4app 仪表板中。 创建一个新类 并添加相关列,例如标题(字符串)和 iscompleted(布尔值)。 您的社交网络后端将使用以下类: 要设置您的数据库: 前往你的 back4app 控制面板上的 "数据库" 部分。 增强用户类 打开现有的 用户 类。 添加列: 生物 (字符串) 头像 (文件) 关注者 (数量,默认:0) 以下 (数字,默认值:0) 创建 post 类 点击 "创建一个班级" 命名为 帖子 并添加: 内容 (字符串) 作者 (指向 用户 ) 图像 (文件) 喜欢 (数字,默认 0) 喜欢的 (数组) 创建评论类 命名为 评论 并包括 内容 (字符串) 作者 (指向 用户 ) 帖子 (指向帖子的指针) 创建对话类 命名为 对话 并添加 参与者 (数组) 最后消息 (字符串) 创建消息类 命名为 消息 并添加 文本 (字符串) 发送者 (指向 用户 ) 对话 (指向对话) 创建 typingstatus 类 命名为 typingstatus 并添加 用户 (指向 用户 ) 对话 (指向对话) 正在输入 (布尔值) 要使用ai代理进行创建,请: 打开ai代理 从您的应用仪表板或菜单中 用简单的语言描述您的数据模型 让ai代理自动创建架构 配置类权限 为了保护您的数据,请调整类级别权限(clps): 前往 "安全与密钥" 在您的仪表板中。 在 "类级别安全" , 设置读/写规则。 例如,允许对 帖子 的公共读取访问,同时限制更新/删除操作仅限于帖子的作者。 通过 livequery 启用实时功能 在 "服务器设置" 中,找到 parse server 并激活 livequery 包括这些类以进行实时监控: 消息 输入状态 帖子 (实时更新点赞和评论) livequery 设置 检索您的应用程序密钥: 导航到 "应用设置" > "安全性与密钥" 保存您的 应用程序 id , javascript 密钥 , 服务器 url , 和 livequery 服务器 url 安全密钥 步骤 2 — 将您的前端与 back4app 连接 设置环境变量 在项目的根目录中创建一个 env local 文件 react app parse app id=your application id react app parse js key=your javascript key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your app id back4app io 用你的实际凭据替换占位符。 配置 parse sdk 创建一个配置文件以初始化 parse // src/utils/parseconfig js import parse from 'parse/dist/parse min js'; // initialize parse with your environment variables parse initialize( process env react app parse app id, process env react app parse js key ); parse serverurl = process env react app parse server url; // enable livequery if configured if (process env react app parse live query url) { parse livequeryserverurl = process env react app parse live query url; } export default parse; 然后在应用程序的入口点导入此文件 (例如, index js ) // src/index js import react from 'react'; import reactdom from 'react dom/client'; import ' /index css'; import app from ' /app'; import ' /utils/parseconfig'; // initialize parse const root = reactdom createroot(document getelementbyid('root')); root render( \<react strictmode> \<app /> \</react strictmode> ); 步骤 3 — 设置用户认证 back4app 的 parse server 提供了一个内置的用户管理系统,通过 parse user 类 parse 如何管理用户 处理用户凭证(用户名、电子邮件、密码) 管理认证状态和会话令牌 简化注册和登录流程 用户注册示例 // function to handle user sign up const handlesignup = async () => { try { const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); // set additional user info user set('bio', ''); user set('followers', 0); user set('following', 0); await user signup(); console log('registration successful'); navigate('/feed'); } catch (error) { console error('sign up error ', error message); // handle errors based on error codes if (error code === 202) { seterrors({ errors, username 'username is taken'}); } else if (error code === 203) { seterrors({ errors, email 'email already exists'}); } } }; 用户登录示例 // function to handle user login const handlelogin = async () => { try { const user = await parse user login(username, password); console log('login successful ', user getusername()); navigate('/feed'); } catch (error) { console error('login error ', error message); setloginerror(error message); } }; 检查现有会话 const checkcurrentuser = async () => { try { const currentuser = parse user current(); if (currentuser) { console log('logged in as ', currentuser getusername()); return currentuser; } return null; } catch (error) { console error('error checking user ', error); return null; } }; 密码重置实现 const handlepasswordreset = async () => { try { await parse user requestpasswordreset(email); console log('reset email sent'); setresetemailsent(true); } catch (error) { console error('password reset error ', error message); setreseterror(error message); } }; 步骤 4 — 处理帖子 创建新帖子 const createpost = async () => { if (!postcontent trim() && !postimage) { console error('please add content or an image'); return; } try { const post = parse object extend('post'); const newpost = new post(); newpost set('content', postcontent); newpost set('author', parse user current()); newpost set('likes', 0); newpost set('likedby', \[]); if (postimage) { const parsefile = new parse file(postimage name, postimage); await parsefile save(); newpost set('image', parsefile); } await newpost save(); console log('post created successfully'); return newpost; } catch (error) { console error('error creating post ', error message); throw error; } }; 步骤 4 — 处理帖子 获取帖子 const fetchposts = async (page = 0, limit = 10) => { try { const post = parse object extend('post'); const query = new parse query(post); query include('author'); query descending('createdat'); query limit(limit); query skip(page limit); const results = await query find(); const posts = results map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, likedby post get('likedby') || \[], createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return posts; } catch (error) { console error('error fetching posts ', error); throw error; } }; 实现点赞/取消点赞功能 const togglelike = async (postid) => { try { const currentuser = parse user current(); const userid = currentuser id; const post = parse object extend('post'); const query = new parse query(post); const post = await query get(postid); const likedby = post get('likedby') || \[]; const isliked = likedby includes(userid); if (isliked) { post set('likedby', likedby filter(id => id !== userid)); post set('likes', math max((post get('likes') || 1) 1, 0)); } else { post set('likedby', \[ likedby, userid]); post set('likes', (post get('likes') || 0) + 1); } await post save(); return !isliked; } catch (error) { console error('error toggling like ', error); throw error; } }; 步骤 5 — 启用帖子评论 添加评论 const addcomment = async (postid, commentcontent) => { if (!commentcontent trim()) { console error('comment cannot be empty'); return; } try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const comment = new comment(); comment set('content', commentcontent); comment set('author', parse user current()); comment set('post', post); await comment save(); console log('comment added successfully'); return comment; } catch (error) { console error('error adding comment ', error); throw error; } }; 获取特定帖子的评论 const fetchcomments = async (postid) => { try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const query = new parse query(comment); query equalto('post', post); query include('author'); query ascending('createdat'); const results = await query find(); const comments = results map(comment => ({ id comment id, content comment get('content'), createdat comment get('createdat'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null } })); return comments; } catch (error) { console error('error fetching comments ', error); throw error; } }; 第6步 — 管理用户资料 检索用户资料 const fetchuserprofile = async (userid) => { try { const query = new parse query(parse user); const user = await query get(userid); const post = parse object extend('post'); const postsquery = new parse query(post); postsquery equalto('author', user); postsquery include('author'); postsquery descending('createdat'); const posts = await postsquery find(); const userdata = { id user id, username user get('username'), bio user get('bio') || '', avatar user get('avatar') ? user get('avatar') url() null, followers user get('followers') || 0, following user get('following') || 0, posts posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') })) }; return userdata; } catch (error) { console error('error fetching user profile ', error); throw error; } }; 更新用户资料 const updateuserprofile = async (profiledata) => { try { const currentuser = parse user current(); if (profiledata bio !== undefined) { currentuser set('bio', profiledata bio); } if (profiledata avatarfile) { const parsefile = new parse file('avatar jpg', profiledata avatarfile); await parsefile save(); currentuser set('avatar', parsefile); } await currentuser save(); console log('profile updated successfully'); return currentuser; } catch (error) { console error('error updating profile ', error); throw error; } }; 第7步 — 实现实时消息传递 back4app 的 livequery 功能支持实时消息传递和输入指示器。 创建对话 const createconversation = async (participantids) => { try { const currentuser = parse user current(); const allparticipantids = \[ new set(\[currentuser id, participantids])]; const existingconversation = await findexistingconversation(allparticipantids); if (existingconversation) { return existingconversation; } const participantpointers = await promise all( allparticipantids map(async (id) => { const userquery = new parse query(parse user); return await userquery get(id); }) ); const conversation = parse object extend('conversation'); const conversation = new conversation(); conversation set('participants', participantpointers); conversation set('lastmessage', ''); await conversation save(); console log('conversation created successfully'); return conversation; } catch (error) { console error('error creating conversation ', error); throw error; } }; // helper to find an existing conversation (simplified for demo) const findexistingconversation = async (participantids) => { try { const conversation = parse object extend('conversation'); const query = new parse query(conversation); const results = await query find(); for (const conversation of results) { const participants = conversation get('participants') || \[]; const ids = participants map(p => p id); if (ids length === participantids length && ids every(id => participantids includes(id))) { return conversation; } } return null; } catch (error) { console error('error finding conversation ', error); return null; } }; 发送消息 const sendmessage = async (conversationid, messagetext) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationquery = new parse query(conversation); const conversation = await conversationquery get(conversationid); const message = parse object extend('message'); const message = new message(); message set('text', messagetext); message set('sender', currentuser); message set('conversation', conversation); message set('read', false); await message save(); conversation set('lastmessage', messagetext); await conversation save(); console log('message sent successfully'); return message; } catch (error) { console error('error sending message ', error); throw error; } }; 通过 livequery 订阅消息更新 const subscribetomessages = async (conversationid, onnewmessage) => { try { const message = parse object extend('message'); const query = new parse query(message); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('sender'); const subscription = await query subscribe(); subscription on('create', (message) => { const newmessage = { id message id, text message get('text'), createdat message get('createdat'), sender { id message get('sender') id, username message get('sender') get('username'), avatar message get('sender') get('avatar') ? message get('sender') get('avatar') url() null }, read message get('read') }; onnewmessage(newmessage); if (message get('sender') id !== parse user current() id) { markmessageasread(message); } }); return subscription; } catch (error) { console error('error subscribing to messages ', error); throw error; } }; const markmessageasread = async (message) => { try { message set('read', true); await message save(); } catch (error) { console error('error marking message as read ', error); } }; 实现输入指示器 const updatetypingstatus = async (conversationid, istyping) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); query equalto('conversation', conversationpointer); query equalto('user', currentuser); let typingstatus = await query first(); if (typingstatus) { typingstatus set('istyping', istyping); } else { typingstatus = new typingstatus(); typingstatus set('conversation', conversationpointer); typingstatus set('user', currentuser); typingstatus set('istyping', istyping); } await typingstatus save(); } catch (error) { console error('error updating typing status ', error); } }; const subscribetotypingstatus = async (conversationid, ontypingstatuschange) => { try { const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('user'); const subscription = await query subscribe(); subscription on('update', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); subscription on('create', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); return subscription; } catch (error) { console error('error subscribing to typing status ', error); throw error; } }; 步骤 8 — 添加搜索功能 用户搜索 const searchusers = async (query, limit = 20) => { try { const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); const users = await userquery find(); const userresults = users map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null, bio user get('bio') || '' })); return userresults; } catch (error) { console error('error searching users ', error); throw error; } }; 帖子搜索 const searchposts = async (query, limit = 20) => { try { const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const postresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return postresults; } catch (error) { console error('error searching posts ', error); throw error; } }; 标签搜索 const searchhashtags = async (tag, limit = 20) => { try { const hashtagquery = tag startswith('#') ? tag substring(1) tag; const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(`#${hashtagquery}\\\b`, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const hashtagresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return hashtagresults; } catch (error) { console error('error searching hashtags ', error); throw error; } }; 步骤 9 — 利用 back4app 的高级功能 back4app 提供了高级工具,如云函数、后台作业和钩子,以丰富您的后端。 服务器端逻辑的云函数 // cloud function example to notify users when a comment is added parse cloud aftersave("comment", async (request) => { if (request original) return; const comment = request object; const post = comment get("post"); const commenter = request user; const postquery = new parse query("post"); const fullpost = await postquery get(post id, { usemasterkey true }); const postauthor = fullpost get("author"); if (postauthor id === commenter id) return; const notification = parse object extend("notification"); const notification = new notification(); notification set("type", "comment"); notification set("fromuser", commenter); notification set("touser", postauthor); notification set("post", post); notification set("read", false); await notification save(null, { usemasterkey true }); }); 使用云代码进行高级搜索 parse cloud define("advancedsearch", async (request) => { const { query, type, limit = 20 } = request params; if (!query) { throw new error("search query is required"); } let results = \[]; switch (type) { case 'users' const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); results = await userquery find({ usemasterkey true }); break; case 'posts' const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery limit(limit); results = await postquery find({ usemasterkey true }); break; default throw new error("invalid search type"); } return results; }); 后台作业用于定期任务 parse cloud job("calculatetrendingtopics", async () => { const post = parse object extend("post"); const query = new parse query(post); const oneweekago = new date(); oneweekago setdate(oneweekago getdate() 7); query greaterthan('createdat', oneweekago); query limit(1000); const posts = await query find({ usemasterkey true }); const hashtagcounts = {}; posts foreach(post => { const content = post get('content') || ''; const hashtags = content match(/#(\w+)/g) || \[]; hashtags foreach(hashtag => { const tag = hashtag tolowercase(); hashtagcounts\[tag] = (hashtagcounts\[tag] || 0) + 1; }); }); const trendingarray = object entries(hashtagcounts) map((\[hashtag, count]) => ({ hashtag, count })) sort((a, b) => b count a count) slice(0, 10); const trendingtopics = parse object extend("trendingtopics"); const trending = new trendingtopics(); trending set("topics", trendingarray); trending set("calculatedat", new date()); await trending save(null, { usemasterkey true }); return "trending topics calculated successfully"; }); 其他高级功能 推送通知: 使用back4app的推送服务提醒用户有关新消息。 基于角色的访问控制: 定义角色以管理内容审核等操作的权限。 webhooks: 通过从云代码触发webhooks来集成外部服务。 步骤10 — 性能优化 随着后端的扩展,性能调优至关重要。考虑以下技术: 数据库索引 在经常查询的字段上创建索引(例如, 作者 , 创建时间 , 用户名 )以提高查询性能。 查询优化 通过仅选择必要的字段并使用分页来限制结果集,从而优化查询。 批量操作 利用批量更新和删除来最小化api调用: const updatemultipleposts = async (postids, updatedata) => { try { const post = parse object extend('post'); const posts = postids map(id => { const post = new post(); post id = id; return post; }); posts foreach(post => { object entries(updatedata) foreach((\[key, value]) => { post set(key, value); }); }); await parse object saveall(posts); console log('posts updated successfully'); } catch (error) { console error('error updating posts ', error); throw error; } }; 缓存策略 为频繁访问的数据实现客户端缓存,以减少api请求。 高效的livequery使用 仅订阅必要的数据,并在数据不再需要时始终取消订阅。 文件处理优化 在上传之前调整图像大小,以节省带宽和存储: const uploadresizedimage = async (originalfile, maxwidth = 1200, maxheight = 1200) => { return new promise((resolve, reject) => { try { const reader = new filereader(); reader onload = (event) => { const img = new image(); img onload = () => { let width = img width; let height = img height; if (width > maxwidth) { height = math round(height (maxwidth / width)); width = maxwidth; } if (height > maxheight) { width = math round(width (maxheight / height)); height = maxheight; } const canvas = document createelement('canvas'); canvas width = width; canvas height = height; const ctx = canvas getcontext('2d'); ctx drawimage(img, 0, 0, width, height); canvas toblob(async (blob) => { const resizedfile = new file(\[blob], originalfile name, { type originalfile type, lastmodified date now() }); const parsefile = new parse file(resizedfile name, resizedfile); await parsefile save(); resolve(parsefile); }, originalfile type, 0 8); }; img src = event target result; }; reader readasdataurl(originalfile); } catch (error) { reject(error); } }); }; 监控和扩展 使用 back4app 的仪表板分析和日志来监控 api 使用情况和性能。 随着用户基础的增长,考虑升级您的计划或实施分片和 cdn 解决方案。 结论 在本教程中,您学习了如何使用 back4app 设置社交网络后端,涵盖用户身份验证、帖子管理、实时消息传递、搜索功能和高级优化技术。 请还阅读我们的博客文章,详细介绍了 如何开发社交媒体应用 祝您编码愉快,祝您的社交网络后端好运!