Создание чата с Firebase
Недавно я запустил Ninjobu, платформу анонимного поиска работы для разработчиков программного обеспечения. Предпосылка заключается в том, что как программист вы создаете профиль с учетом своего предыдущего опыта, используемых языков программирования, знания предметной области и т. д, а также того, какую роль вы ищете, предпочитаемое местоположение (или удаленное) и желаемую зарплату. Затем вы расслабляетесь и ждете, пока рекрутеры будут искать в списке профилей подходящих кандидатов на свои открытые должности. Как только рекрутер нашел потенциально подходящего кандидата, он устанавливает контакт и начинает процесс найма, который приводит нас к системе чата.
Ninjobu нужен только способ для рекрутеров, чтобы начать чат с кандидатами один на один. Нет необходимости в группах, обмене файлами, смайликах, гифках или подобных функциях. Хотя вы можете отправлять смайлики нормально, пользовательского интерфейса для их выбора нет, и это скорее счастливая случайность из-за того, что Firestore поддерживает Unicode, а не из-за того, что я специально реализовал. Отсутствие этих дополнительных функций несколько упрощает требования к системе чата, но мы кодируем именно то, что нам нужно, и ничего больше, не так ли?
Дизайн данных
Я выбрал дизайн, в некоторой степени вдохновленный примером ценообразования Firebase. Первичные данные состоят из набора чатов, где каждый чат имеет подколлекцию сообщений.
В каждом чате есть два участника, потому что в Ninjobu нужно общаться только один на один. Распространить это на группы с дополнительными участниками - тривиально. Тем не менее, поскольку я не предвижу необходимости в обсуждениях с участием нескольких участников в этом проекте, некоторые части кода иногда предполагают, что существует только два участника чата.
Массив members
содержит две записи: уникальный идентификатор пользователя для каждого участника чата. С помощью этих UID мы можем найти чаты для конкретного пользователя и заблокировать доступ к данным для всех остальных. Ninjobu использует Firebase Auth для аутентификации пользователей, а я использую UID из модуля Auth для идентификации каждого пользователя в данных чата.
В массиве names
хранятся имена участников чата, а в массиве desc
- вторичная строка для каждого пользователя. Для рекрутера это название компании, а для кандидата - его предпочтительная должность. Сохранение этих строк в документе чата позволяет нам дать каждому чату немного контекста в почтовом ящике пользователя без запроса каких-либо дополнительных данных.
Ninjobu стремится сохранить анонимность своих кандидатов-пользователей. Кандидатов даже не спрашивают их имена при регистрации. При общении с кандидатом его имя отображается как кандидат # xyz, а имя и компания рекрутера отображаются, как и ожидалось.
Наконец, у нас есть строка с последним сообщением, отправленным в этот чат, и отметкой времени, когда это произошло. Имея эти данные в документе, мы можем запросить список чатов и отобразить их так же, как приложение электронной почты может показывать пользователю список электронных писем. Когда пользователь выбирает чат, мы при необходимости извлекаем подколлекцию сообщений.
Данные сообщения просты. В каждом документе хранится UID автора, временная метка сообщения и сама строка. UID используется для проверки индекса автора в родительском ЧАТЕ в массиве members
, а индекс карту с именем автора и вторичной строкой в массивах names
и desc
, соответственно. Вы можете сохранить здесь индекс члена вместо UID, но в моем случае использование UID упростило некоторый интерфейсный код.
Firebase рассчитывает стоимость на основе количества прочитанных, удаленных и обновленных документов, а также использования пропускной способности сети и хранилища. При такой настройке мы дублируем некоторые данные, такие как последнее отправленное сообщение, но это позволяет нам запрашивать меньше документов в целом. Обратной стороной является то, что когда мы публикуем новое сообщение чата, нам нужно создать новый документ в подколлекции сообщений и обновить родительский документ чата, указав последнюю строку сообщения и временную метку. Я все еще думаю, что это приемлемо. Предположительно, будет гораздо больше операций чтения документов от пользователей, которые входят в систему и просматривают свои входящие сообщения, чем от пользователей, отправляющих новые сообщения.
Сохранение конфиденциальности данных
Данные Firestore зашифровываются при сохранении на жестких дисках, а Ninjobu перенаправляет любые незашифрованные запросы на HTTPS, что означает, что наши данные должны оставаться в безопасности при передаче и хранении. Но нам все равно нужно сделать так, чтобы чаты были видны только их участникам. Мы делаем это с помощью превосходной системы правил безопасности Firebase.
Firestore интегрируется с Firebase Auth, чтобы упростить нашу жизнь. Поскольку документы чата имеют массив members
с UID каждого участника чата, мы можем написать правило безопасности, которое дает доступ только участникам чата.
Для неаутентифицированных запросов доступ к переменной request.auth.uid
будет недопустимым, и правило не сработает. В противном случае мы разрешаем доступ к документам чата, содержащим UID запрашивающего в массиве members
.
К сожалению, с этой настройкой есть крошечный сбой. Когда мы впервые создаем документ, ресурс не существует, и у нас нет массива элементов для проверки, поэтому правило не выполняется. Нам нужно относиться к созданию документа как к особому случаю, что мы можем сделать.
С обновленным правилом мы разрешаем чтение и обновление документа чата, когда UID запрашивающего существует в массиве members
, уже записанном в базе данных. Однако, когда документ создается, мы ожидаем, что запрос будет содержать массив с двумя записями с UID запрашивающей стороны.
Для вложенных документов нам нужны независимые правила, потому что каждое правило применяется только к указанному пути. Правило чата не повлияет на подколлекцию сообщений. Однако мы все еще можем использовать массив members
в родительском документе чата при защите документов сообщений.
Функция get()
позволяет читать любой документ в базе данных. В приведенном выше правиле мы получаем доступ к родительскому чату и проверяем, является ли пользователь его участником, имея в виду, что доступ к документу в правиле безопасности учитывается при выставлении счетов как обычный документ, прочитанный вручную.
Чат в реальном времени
Вы можете получить данные из базы данных в любое время, но поскольку мы пытаемся создать систему чата, мы хотим, чтобы обновления происходили как можно скорее. Если вы зависаете на своей странице «Входящие», и кто-то отправляет вам сообщение, вы хотите сразу увидеть его, не перезагружая страницу.
Firestore предоставляет способ зарегистрировать обработчика для определенного запроса и затем вызывать его со снапшотами каждый раз, когда изменяются данные, затронутые этим запросом. Это невероятно удобно, поскольку обновление данных происходит в режиме реального времени без дополнительной работы по синхронизации состояния между пользователями. Таким образом, чат ведет себя больше как чат, а не как медленное электронное письмо.
Есть одно предостережение, которое я хотел бы упомянуть, чтобы вам не приходилось тратить время на отладку, как это сделал я, если бы вы использовали аналогичную реализацию, что и моя. Если вы строите свой пользовательский интерфейс на основе обработчиков снапшотов, одним из недостатков будет высокая задержка, вызванная обращением к серверу базы данных. Когда вы отправляете сообщение, оно отправляется на сервер, база данных обновляется, и только после этого срабатывает обратный вызов снапшота с обновлением. Этого времени обновления достаточно для общения в чате, но оно будет слишком медленным с точки зрения отзывчивости пользовательского интерфейса. К счастью, люди из Firebase предоставляют отличное решение для компенсации задержки.
В Firestore изменение базы данных сначала применяется локально, а затем отправляется на сервер. Обработчик снапшотов сработает до того, как сервер зафиксирует изменение, что позволяет быстро обновлять пользовательский интерфейс. Вы можете проверить это в своем обратном вызове, если хотите, просмотрев метаданные снимка для флага hasPendingWrites. Важно понимать, что локальное изменение не взаимодействует с сервером и не может выполнять правила безопасности. Это не проблема, если вы хорошо спроектируете код, но я этого не делал, поэтому у меня возникла проблема.
Когда рекрутер на Ninjobu впервые отправлял сообщение кандидату, он создавал новый документ чата и подколлекцию с этим первым сообщением. Затем в обработчике снапшотов для документов чата он обнаружил новый чат и немедленно прочитал его подколлекцию сообщений для отображения на странице. Если вы помните наши правила безопасности ранее, мы сначала смотрим на документ родительского чата и проверяем, является ли пользователь участником этого чата. В этом случае правило не сработает, потому что мы запросили коллекцию сообщений для документа чата, который был создан только локально и еще не был записан на сервере базы данных. Решение было простым: немедленно отобразить сообщение локально, но не запрашивать вложенную коллекцию из базы данных, пока запись документа чата не завершится успешно.
Но подождите, это еще не все
Я хотел добавить визуальную подсказку, когда у пользователя появляются новые сообщения. Я решил поставить крошечный значок рядом с заголовком папки «Входящие», чтобы указать на непрочитанные сообщения и отметить каждый чат, который пользователь еще не прочитал. Чтобы это сработало, мне нужно было добавить некоторые дополнительные данные в документ чата.
Я добавил новый массив из двух записей lastSeenTime
с меткой времени для каждого участника чата, которая указывает, когда он в последний раз читал чат. Обнаружить новое сообщение сейчас просто: мы сравниваем отметку времени последнего сообщения с отметкой времени последнего посещения для участника. Когда пользователь открывает чат и получает все сообщения или когда чат открыт, мы обновляем метку времени последнего посещения. Мы также можем использовать эти поля, чтобы показать, когда получатель увидел сообщение, но мне не очень нравится этот аспект программного обеспечения чата, поэтому я не добавил его в Ninjobu.
В заключении
Firebase позволяет невероятно легко создать компонент чата для вашего веб-приложения. Дизайн в этом посте может быть не лучшим способом реализации такой системы, и, возможно, он даже не очень эффективен. Тем не менее, он хорошо зарекомендовал себя для Ninjobu, и я считаю, что он будет достаточно хорошо масштабироваться для нужд этой платформы. Если я ошибаюсь, вероятно, будет другой пост с внесенными улучшениями.
В этой системе чата отсутствует одна вещь, которую я хотел бы создать в следующий раз. Когда пользователь получает сообщение, он должен войти в систему, чтобы узнать об этом. В идеале мы отправляем уведомление по электронной почте, чтобы сообщить им, что их ждет новое сообщение. Как только я это построю, возможно, это станет темой следующего сообщения в блоге.