Project Templates
Social Network
Как построить бэкенд для социальной сети?
48 мин
введение в этом руководстве вы узнаете, как настроить надежный бэкенд для социальной сети, используя платформу бэкенда как услугу этот бэкенд будет разработан для обработки аутентификации пользователей, управления данными, связи в реальном времени и многого другого — все это с обеспечением безопасности и эффективности к концу этого учебника ваш проект будет выглядеть так обзор социальной сети совет для получения полного исходного кода посетите наш репозиторий на github по адресу github https //github com/templates back4app/back4gram основные выводы упрощенное управление бэкендом используйте управляемый бэкенд back4app для оптимизации аутентификации пользователей, хранения данных и функций в реальном времени взаимодействия в реальном времени используйте livequery для динамического обмена сообщениями и немедленных обновлений контента комплексное управление пользователями и контентом создайте надежные системы для профилей пользователей, постов, комментариев и социальных взаимодействий масштабируемость и производительность применяйте оптимизацию запросов, кэширование и пакетные операции для поддержания высокой производительности по мере роста вашей сети расширенные интеграции улучшите свой бэкенд с помощью облачных функций, фоновых задач и push уведомлений для более богатого пользовательского опыта предварительные требования перед началом убедитесь, что у вас есть учетная запись back4app зарегистрируйтесь бесплатно на back4app com https //www back4app com/ node js и npm установите node js (v14 x или новее) с nodejs org https //nodejs org/ базовые знания javascript и react редактор кода используйте редакторы, такие как visual studio code или sublime text шаг 1 — настройка вашего бэкенда back4app создание нового проекта на back4app войдите в свою панель управления back4app нажмите на "создать новое приложение" назовите ваше приложение (например, "back4social") и выберите регион сервера, который ближе всего к вам после создания проекта вы увидите его в списке на вашей панели управления back4app этот проект станет основой для всех конфигураций бэкенда, обсуждаемых в этом руководстве проектирование схемы базы данных ваш бэкенд социальной сети будет использовать следующие классы пользователь (предоставляется parse по умолчанию) расширьте с полями, такими как биография и фотография профиля пост содержит текстовый контент и загрузки изображений комментарий хранит комментарии пользователей к постам разговор представляет собой сеанс чата между пользователями сообщение содержит отдельные сообщения в разговоре статуспечати указывает, когда пользователь печатает добавление классов базы данных в back4app данные хранятся в классах вы можете создать новый класс в панели управления back4app или через ai агента чтобы создать с помощью панели управления, пожалуйста перейдите в раздел “база данных” в вашей панели управления back4app создайте новый класс и добавьте соответствующие столбцы, такие как заголовок (string) и iscompleted (boolean) ваш бэкенд социальной сети будет использовать следующие классы чтобы настроить вашу базу данных перейдите в "база данных" раздел на вашей панели управления back4app улучшить класс user откройте существующий пользователь класс добавить столбцы био (строка) аватар (файл) подписчики (количество, по умолчанию 0) следующий (число, по умолчанию 0) создайте класс post нажмите "создать класс" назовите это пост и добавьте содержимое (строка) автор (указатель на пользователь ) изображение (файл) нравится (число, по умолчанию 0) нравится (массив) создайте класс comment назовите это комментарий и включите содержимое (строка) автор (указатель на пользователь ) пост (указатель на пост) создайте класс conversation назовите это разговор и добавьте участники (массив) последнеесообщение (строка) создайте класс message назовите это сообщение и добавьте текст (строка) отправитель (указатель на пользователь ) разговор (указатель на разговор) создайте класс typingstatus назовите это typingstatus и добавьте пользователь (указатель на пользователь ) разговор (указатель на разговор) печатает (булевый) чтобы создать с помощью ai agent, пожалуйста откройте ai agent из вашей панели управления приложением или меню опишите вашу модель данных простым языком позвольте ai agent создать схему автоматически настройка разрешений класса чтобы защитить ваши данные, настройте разрешения уровня класса (clp) перейдите в "безопасность и ключи" , в вашей панели управления в разделе "безопасность уровня класса" , установите правила чтения/записи например, разрешите публичный доступ для чтения для пост , ограничив операции обновления/удаления автором поста включение функций в реальном времени с помощью livequery в "настройки сервера" , найдите parse server и активируйте livequery включите эти классы для мониторинга в реальном времени сообщение статус набора текста пост (для обновления лайков и комментариев в реальном времени) настройка livequery получите свои ключи приложения перейдите в "настройки приложения" > "безопасность и ключи" сохраните свой id приложения , javascript ключ , url сервера , и url сервера livequery ключи безопасности шаг 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 — настройка аутентификации пользователей parse server от back4app предлагает встроенную систему управления пользователями через класс 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; } }; получение постов 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 — реализация обмена сообщениями в реальном времени функция livequery от back4app позволяет обмениваться сообщениями в реальном времени и отображать индикаторы ввода создание разговора 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"; }); дополнительные расширенные функции push notifications используйте сервис push уведомлений back4app, чтобы уведомлять пользователей о новых сообщениях role based access control определите роли для управления разрешениями на действия, такие как модерация контента webhooks интегрируйте внешние сервисы, вызывая вебхуки из cloud code шаг 10 — оптимизация производительности по мере масштабирования вашего бэкенда настройка производительности становится ключевой рассмотрите следующие техники индексация базы данных создайте индексы на часто запрашиваемых полях (например, author , createdat , username ) для повышения производительности запросов оптимизация запросов оптимизируйте запросы, выбирая только необходимые поля и используя пагинацию для ограничения наборов результатов пакетные операции используйте пакетные обновления и удаления, чтобы минимизировать вызовы 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, охватывающий аутентификацию пользователей, управление постами, обмен сообщениями в реальном времени, функции поиска и продвинутые методы оптимизации пожалуйста, также прочитайте наш блог, в котором подробно описано как разработать приложение для социальных сетей счастливого кодирования и удачи с вашим бэкендом социальной сети!