Получение списка пользователей и их вывод на ui

Получение списка пользователей и их вывод на ui

Введение

Всем привет, меня зовут Александр, я являюсь фронтенд разработчиком с 4-х летним опытом работы. В этой статье хотел поделиться как разрабатываю модуль пользователей и собеседников для моего проекта веб сокетов. В этой статье вы узнаете, как я создал моковые данные пользователей, почему во время разработки я несколько раз менял структуру базы данных, как данные отдаются на сторону юая, обрабатываются и затем выводятся в готовом виде пользователю.

Генерация моковых данных

Генерировать моковые данные решил через консольное приложение seeder на nest js, ранее я описывал, как это работает в статье «ссылка на статью Консольное приложение в бекенде». Сейчас я только вкратце пробегусь по функционалу и не буду на этом задерживаться.

Для работы с базой данных я создал два файла репозитория: для пользователя и сообщений между ними. Их задача заключается в занесении записей в базу данных. Оба они имеют метод публичный метод create, который принимает в себя массив данных, в первом случае — это массив пользователей, во втором — массив сообщений.

<p> Занесение моковых пользователей в базу данных</p>

Занесение моковых пользователей в базу данных

<p> Занесение сообщений в базу данных</p>

Занесение сообщений в базу данных

Как видим из вышеперечисленных картинок, то они работают по одному принципу: получают на вход массив заготовленных данных для занесения в базу данных, проходятся по массиву и создаем новые модели пользователей и сообщений соответственно. После этого данные заносят в базу данных, в результате их занесения в хранилище получаем массив сохраненных данных на выходе.

Далее, чтобы не путаться с логикой между сообщениями и пользователями, мне пришлось создать еще два файла сервисов. Их задача заключалась в описании создания моковых данных и подготовке их к передаче в репозитории для занесения в базу данных. Сейчас остановлюсь на общих моментах, чтобы описать логику работы этих сервисов.

<p> Главная функция запуска сервиса пользователей, которая вызывается из файла seeder.ts</p>

Главная функция запуска сервиса пользователей, которая вызывается из файла seeder.ts

На картинке выше описывается порядок действий, которые необходимо сделать, чтобы корректно создать пользователя. Для начала необходимо сгенерировать всех пользователей, которые будут занесены в базу данных — это действие выполняет функция generateNewUsers, для генерации пользователя была использована библиотека faker.js. Далее, мне необходимо создать лог файл, в котором будут хранится все пользователи в виде «username:password». Это необходимо, чтобы я смог потом скопировать данные, чтобы зайти в чат. Если этого не сделать, то потом не узнаю какой пароль был сгенерирован, а все из-за того, что перед помещением пользователя в бд необходимо его пароль захешировать, этой задачей занимается функция preparePasswordUsers. Для хеширования пароля использую библиотеку argon2.

После всех вышеописанных процедур, наконец-то, заношу данные пользователей в базу данных через репозиторий, который описан выше.

<p> Главная функция запуска сервиса сообщений, которая вызывается из файла seeder.ts</p>

Главная функция запуска сервиса сообщений, которая вызывается из файла seeder.ts

Так как сообщения зависят от пользователей, то их генерация происходит после генерации пользователей, далее на вход функции runGenerateMessages поступает массив сгенерированных пользователей. Для корректной работы сообщений мне необходимо выделить одного главного пользователя, который будет общаться со всеми остальными, да вот такой очень общительный парень получается. Для этой цели я беру первого пользователя в массиве и далее исключаю его из него, чтобы он сам с собой не общался.

Генерацией сообщений здесь построена иначе, чем пользователей. В одной итерации мне возвращается массив коллекций. Одна коллекция содержит в себе два сообщения. В первом сообщении отправителем выступает главный пользователь, а получателем — один из пользователей в массиве, во втором сообщении — все наоборот.

Генерация сообщений
Генерация сообщений

Как видно на картинке выше, за один цикл генерируется два сообщения, это можно увидеть в функции generateOneTwoOneMessages, также хочу отметить, что в функции пользователи меняются местами отправителя и получателя. Таким образом получам вышеописанный массив коллекций и далее его переводим в простой массив. Этот массив далее заносится в базу данных.

<p> Файл seeder.ts</p>

Файл seeder.ts

На вышеописанной картинке сам файл, который запускается из консоли, как видите, я всю логику вынес в сервисы и репозитории.

Формирование списка собеседников

Когда начал продумывать формирование списка собеседников, то представлял, что по токену получу username пользователя и затем самого пользователя. Далее через пользователя получу всех его собеседников из сообщений. После этого уберу дублирующихся собеседников и в финале получу массив отсортированных собеседников по дате последнего сообщения в диалоге.

<p> Экшен в контроллере, который отвечает за формирование списка собеседников</p>

Экшен в контроллере, который отвечает за формирование списка собеседников

Как видим, получить username получилось из сервиса по работе с токенами, самого пользователя будем получать в сервисе пользователей.

В самом сервисе пользователя через репозиторий пользователя получил экземпляр пользователя из базы данных и далее в сервисе сообщений хотел получить массив собеседников пользователя. Вот на этом моменте хочу остановится подробнее.

<p> Попытка получить готовый массив из обращения в базу данных</p>

Попытка получить готовый массив из обращения в базу данных

Для формирования итогового массива я испробовал разные варианты запросов. Если получить данные через find, то их можно будет отсортировать и пустить через пагинацию, но в данном случае поиск происходит по первым пяти отсортированным данным, в которых, в придачу, могут быть дубли среди собеседников.

Если искать через distinct, то получаем только массив собеседников, казалось, что еще нужно для счастья? Но этот массив не будет отсортирован по полученному последнему сообщению и толку от этого списка нет. После изучения документации, оказывается, что с distinct, нельзя применять сортировку и пагинацию, потому что с ним эти параметры не работают, этим теперь и объясняется почему я ранее получал не работоспособный массив.

Аналогичная ситуация получается и если построить запрос в бд через aggregate — получаю массив не отсортированных собеседников.

На основе вышеописанного у меня получилось сформировать два варианта решения вопроса:

1) Получаем все сообщения из базы данных и далее с ними работаем напрямую;

2) Создаю отдельную коллекцию в базе данных в рамках которой будет хранится пользователь и все его собеседники, у собеседников будет хранится ссылка на последнее сообщение в чате.

В первом варианте необходимо получить все сообщения и далее их отсортировать, а из остатков вычленить нужные данные для работы. Если в базе данных немного сообщений, то первый вариант подойдет, немного — это 10-15 тысяч сообщений. Если сообщений несколько сотен тысяч или миллионов, то тут начнутся подвисания и это уже не очень хорошо для системы.

Во втором варианте по запросу собеседников пользователю сразу выдаст готовый список, который необходимо будет только отсортировать. Также, имея на руках ссылку о последнем сообщении, можно вывести нужные данные по сообщению.

Очевидно, что второй вариант выигрывает и по производительности, и по расширяемости и дополнению данных, и по поддержке, поэтому в реализацию пойдет второй вариант.

Для реализации второго варианта была создана дополнительная коллекция документов Interlocutors в базе данных. Данные в нее заносились по следующему принципу: создавался объект с ключами userId и interlocutors. Userid — это айдишник пользователя, interlocutors — массив объектов, сам объект в массиве состоял из следующих ключей: interlocutorId, messageId и createdAt. Interlocutorid — это айдишник собеседника, данные по собеседнику хранятся в коллекции пользователей, messageId — айдишник сообщения, createdAt — время последнего сообщения в их переписке. CreatedAt здесь нужен для того, чтобы сразу отсортировать данные в массиве, а не запрашивать данные из коллекции Messages и потом отсортировать их по дате.

<p> Поиск собеседников пользователям</p>

Поиск собеседников пользователям

Как видно на картинке, в аргументы функции попадает айдишник пользователя и через него находят всех его собеседников в коллекции Interlocutors. Далее массив сортируется по дате от сообщения и вырезается нужная часть и возвращается для дальнейшей работы.

<p> Формирование списка собеседников для юая</p>

Формирование списка собеседников для юая

Как видно из картинки выше, после получения айдишников собеседников и сообщений можно получить необходимые данные и сформировать полноценный массив собеседников, который в последствии будет отправлен на юай.

Получение данных на стороне юая и их отрисовка

На стороне юая я решил выделить собеседников в сервис юзеров.

<p> Запрос данных для отрисовки собеседников пользователям</p>

Запрос данных для отрисовки собеседников пользователям

На картинке выше описывается функционал, который отвечает за запрос по собеседникам пользователя. Здесь используется обычный функционал rtq query, подробно я здесь останавливаться не буду, полную информацию можете посмотреть в документации.

<p> Отрисовка списка через компонент dataScroller из ui кита primereact</p>

Отрисовка списка через компонент dataScroller из ui кита primereact

Для отрисовки данных я использую готовый ui кит primereact для экономии времени, чтобы было больше времени уделять своей логике и максимально ускорить свою работу. Из удобного здесь это то, что в этом элементе ui кита мне понравилось организации шаблона (template). В его аргументах передается все, что находится в элементе массива. А заострить внимание я хочу на компоненте Footer, в него передается два пропса, которые работают с пагинацией.

<p> Реализация футера</p>

Реализация футера

В списке собеседников я хотел организовать бесконечный подгрузку данных через нажатие по кнопке. Когда изменяется входящие аргументы для запроса данных — сразу запускается новый запрос. Проблема в том, что rtk query заменяет старые данные новыми, которые были получены в результате нового запроса, а мне нужно, чтобы новые данные соединялись в один массив со старыми. Если вы обратили внимание, то на картинке «Запрос данных для отрисовки собеседников пользователям» можно увидеть свернутые функции serializeQueryArgs, merge, forceRefetch. Я их пробовал использовать, чтобы соединить дополнить полученных собеседников новыми, но ничего из этого не помогло, сейчас хотел описать, что каждая функция делала, и что пытался добиться с их помощью.

SerializeQueryArgs — это управление кешем, в найденных примерах предлагалось сохранять данные, при этом оставлять старые аргументы. Идея заключалась в том, чтобы сохранять функцию с одними и теми же аргументами запроса.

Merge — дополнять старые данные новыми. То есть в ней было два аргумента с полученными и новыми данными соответственно. Но я даже не попал в эту функцию, почему так происходит мне понять так и не удалось.

ForceRefetch — здесь вообще, какая-то магия происходит. После добавления этой функции у меня перестает работать запрос, почему так происходит также не получилось разобраться.

<p> Обработка данных через адаптары в createApi</p>

Обработка данных через адаптары в createApi

Была еще попытка организовать подгрузку данных через адаптор. Для этого необходимо создать адаптор, далее через него инициализируем данные. После этого в функции transformResponse прогоняем данные для того, чтобы они приобрели нужный вид для работы с адаптором и были занесены в стор.

Далее из адаптора необходимо вытащить селекторы и указать из какого хранилища будут браться данные для работы с селекторами. По итогу получилось, что вернулся к тому месту откуда ушел в прошлый раз — новые данные перетирают старые и их нельзя объединять.

Основываясь на том, что ничего не помогает соединить данные в рамках функции createApi. Из найденных решений самое адекватное, что я нашел — это напрямую вынести запрос из сервиса и после его завершения добавлять новые данные к старым.

<p> Прописывание логики для slice</p>

Прописывание логики для slice

После того, как данные будут получены из запроса их необходимо где-то хранить. Для этого я создал дополнительный файл и в нем прописал логику slice, которая включает в себя экшены, редюсеры, селекторы.

Как видно из картинки выше, сначала задается имя, по которому будут доступны данные в сторе, начальное состояние и экшены для работы со стором. Далее все это извлекается для дальнейшей работы, также в файле сразу указан селектор.

<p> Хук для работы с пагинацией</p>

Хук для работы с пагинацией

Чтобы инкапсулировать всю логику работы с сущностью пользователя — был создан хук. Его задача — это полное прописание логики пагинации и загрузки. Как видно из картинки выше, в нем получаем данные по собеседникам пользователя, изначальное состояние пагинации и логику ее изменения, а также вызов функции подгрузки данных о собеседниках.

<p> Логика загрузки собеседников</p>

Логика загрузки собеседников

Как видно из картинки выше, логику для запроса данных получилось вынести в отдельную санку, в рамках которой, как раз, и получилось сделать запрос и занести его результаты в глобальный стор.

В результате, если все вышеперечисленное на стороне юая сложить в единую картину, то получаем следующую картину: в компоненте списка собеседников вставляем хук, который отвечает за логику данных списка собеседников, в нем после первой отрисовки происходит запрос на получение списка пользователей и, в нем же, пишем колбек, который будет в дальнейшем запрашивать пользователей по нажатию на кнопку. Из хука извлекается список, состояние загрузки и ранее описанный колбек и далее все раскидывается в родительском компоненте по своим местам.

<p> Измененный футер после правок запроса на получение данных по собеседникам пользователя</p>

Измененный футер после правок запроса на получение данных по собеседникам пользователя

Одним из таких мест стал компонент футера. Из него получилось убрать формирование запроса и лишние пропсы. В итоге остался один пропс, который отвечает за уже готовую функцию запроса и его необходимо поместить в колбек на клик кнопки.

Вывод

В данной статье получилось затронуть очень много тем. Мы увидели пример алгоритма, по которому можно сделать бекенд для генерации данных и его обработку перед тем, как отдать в работу на сторону юая. На этом примере хорошо видно, что сразу все предвидеть и сделать без ошибок не получается. Только у меня в ходе разработки этого функционала из-за того, что параллельно изучаю и пробую в работе rtk query и nestjs более глубже, были затыки со структурой базы данных и как из этого выйти, проблемы с запросом из базы данных и продумывание ее решения, решение проблемы перезатирания данных на стороне юая между запросами. И все это решалось, да не бысто, не сразу, но решение находилось.

Удалось более лучше пощупать работу с запросами и более глубже понять различие между реляционными и нереляционными базами данных. Разобраться более подробно в работе rtk query, когда можно использовать, то что есть из под коробки, а когда что-то лучше дописать. Ознакомился со слайсами и адаптарами в redux.

Спасибо за то, что провели это время со мной и дочитали мою статью, до встречи в следующих статьях на страницах моего блога.

66 показов
2121 открытие
Начать дискуссию