

Томас Безуска из Step Up Labs поделился опытом компании по использованию Cloud Functions Firebase в приложении Settle Up, которое делит совместные платежи между участниками группы и сохраняет историю долгов и расходом для каждого из группы.
В марте Google анонсировал бета-запуск облачных функций (Cloud Functions) для Firebase. Вкратце – это небольшие фрагменты JavaScript-функций, развернутых на серверах Firebase и выполняющихся для разных событий, таких, например, как изменения базы данных Firebase или новый логин пользователя.
Мы в Step Up Labs работали с Google в течение прошлого года и очень рады видеть, что продукт наконец переживает публичный бета-запуск. Так как я работаю с облачными функциями уже некоторое время, я думаю, что мог бы поделиться некоторыми деталями того, как мы используем их в нашем главном продукте Settle Up, и приобретенным опытом. Я не буду описывать API или описывать в деталях, как работать с Functions, для этого есть официальные документы и примеры кода.
О Settle Up
Позвольте мне сначала кратко рассказать вам, что делает приложение. Settle Up – это приложение для всех, кто хочет отслеживать групповые расходы. Это идеально для путешественников, соседей и друзей, которые не хотят спорить о том, кто кому платит и кто кому должен.
В приложении вы можете создавать отдельные группы (например, “лыжная поездка 2017”). В каждой группе вы можете добавлять участников и индивидуальные расходы (бензин, билеты, отель и т.д.). На основании этих расходов приложение сообщит вам в конце поездки, кто должен заплатить кому. Одна из полезных функций – индивидуальные расходы могут быть в разных валютах, а долги будут отображаться в ваших “родных” деньгах.
Вся эта информация – группы, участники, расходы и другие данные – управляются отдельными клиентскими приложениями и хранятся в базе данных Firebase. Однако существуют некоторые важные части, которые мы решили перенести в Cloud Functions.
Реакция на групповые изменения
Одна из вещей, которую мы хотим показывать пользователям Settle Up – это изменения, сделанные в каждой группе. Например, когда кто-то удаляет расход, должна быть запись, показывающая, кто её удалил. Это идеальный случай использования Cloud Functions. Отрывок ниже показывает, как мы это сделали (упрощен для ясности):
Несколько комментариев:
- Мы используем Typescript, который транскомпилируется в чистый JavaScript (сейчас ES6). Это значительно упростило разработку во время пре-бета фазы, когда API менялся довольно часто, потому что компилятор сразу сообщал нам, что неисправно. Я очень рекомендую даже с бета-версией использовать Typescript, потому что это повышает вашу продуктивность без затрат.
- Мы используем async/await, что упрощает код, убирая раздражающие коллбеки и приводя его к виду любого синхронного кода. В данный момент это функция есть только в Typescript, которая компилируется в генераторы. Хорошо, что она использует Promises, с которым Cloud Functions работает по умолчанию.
- Мы используем шаблон admin.database.ServerValue.TIMESTAMP для установки времени сервера.
- Мы должны узнать, кто внес изменение. Для этого пока нет задокументированного способа, только незадокументированный – пользовательский ID устанавливается внутрь event.auth.variable.uid. Существуют ситуации, когда так сделать нельзя (например, при внесени изменений прямо в консоли Firebase), поэтому вам нужна проверка для этого, иначе переменная by становится undefined и не может быть сохранена в базе данных Firebase.
Отправка пуш-уведомлений
Следующий шаг после внесения изменения в приложение – отправить пуш-уведомление подписанным клиентам. Когда пользователь входит со своего устройства, приложение требует токен регистрации и сохраняет его в базе данных. ВОт как выглядит наша структура:
На github есть хороший официальный пример того, как отправлять пуш-уведомления через Firebase. Я просто подчеркну несколько вещей:
- Используйте Admin SDK Firebase для отправки уведомлений, так вам не нужно будет иметь дело с аутентификацией на серверах Firebase и делать ручные HTTP-запросы.
- Вы не можете отправлять null в payload.
- Обработайте результаты send ..() методов и удалите токены, которые FCM-серверы помечают, как недействительные (как в примере с github).
- Разработайте разные уведомления для разных платформ. Обратите внимание, что у нас есть информация о платформе для каждого уведомления, что позволяет нам создать разную полезную нагрузку для каждой платформы. Структура описана здесь.
Этот сниппет показывает два типа полезной нагрузки – raw, которая отправляется в веб-приложение и на устройства Android, и для iOS, которая отправляется пользователям iPhone. “Сырая” содержит обычный JSON, который обрабатывается устройством в фоновом режиме и объединяет схожие уведомления в одно. Это работает так же и в нашем веб-приложении.
С другой стороны, payload для iOS не допускает обработку и представляется на устройстве в том виде, в котором получен. Использование свойств body_loc и title_loc позволяет убедиться, что уведомления размещено правильно.
Работа с обменными курсами
Так как Settle Up позволяет вводить расходы в разных валютах, нам нужен был способ работы с обменными курсами и приведением всех затрат в одну валюту. Для этого требовались соединить три части головоломки:
- получать обменный курс между валютой расхода и валютой группы для каждой траты;
- централизованное хранилище обменных курсов в базе данных Firebase;
- изменять валюту группы для того, чтобы показывать долги в другой валюте.
Единое хранилище обменных курсов в базе данных упрощает разработку клиентских приложений – чтобы получить курс обмена, им нужно только получить доступ в одну локацию Firebase и больше ни о чем не беспокоиться. В этой локации есть курсы обмена для любой мировой валюты и доллара, которые обновляются каждый день и сохраняются в истории. Существует также специальный узел под названием “последние”, в котором хранятся новейшие курсы валют.
Чтобы собирать последние данные каждый день, мы используем HTTPS-триггер в Cloud Functions и CRON job Google App Engine, который запускает его каждый день.
Я хотел бы также отметить response.end(), который правильно закрывает входящие HTTPS-запросы. Без него запросы будут просрочены.
Как вы можете видеть, мы используем Yahoo как источник курсов, но это деталь, которую можно легко изменить в Cloud Functions без необходимости изменений в клиентских приложениях.
Изменение валюты группы
Менять валюты группы не всегда просто. Перед изменением мы должны пересмотреть все расходы в группе и обновить обменные курсы. Только после этого мы можем обновить валюту группы. Очевидно, что это должно быть функцией сервера.
Мы решили выделить локацию в базе данных Firebase под названием serverTasks и сделать облачную функцию, следящую за этой локацией и запускающую нужные функции. Конечно, это можно было сделать HTTPS-запросом (как в случае обновления курсов валюты), но это бы усложнило клиентский код. Гораздо проще было просто создать локацию в Firebase и наблюдать за ней, чтобы получить результат операции.
Вот как может выглядеть структура базы данных:
И вот функция для изменения валюты группы вместе с регистрацией:
Снова несколько приимечаний:
- Мы используем adminRef, чтобы писать код ответа, потому что правила Database не разрешают кому-либо его записывать. Так задумано, потому что мы хотим, чтобы там могли писать только Cloud Functions. В целом всегда хорошо провести немного времени, поразмышляв о правилах доступа и их применении.
- Функция записывает обновления в самом конце в виде обновления мультилокаций. Это полезно по двум причинам: это гарантирует, что мы не получим повреждений в случае ошибки при обработке, и это быстрее.
- Перед записью данных функция проверяет, существует ли оригинальное задание. Если нет, это значит, что обработка задачи заняла так много времени, что приложение клиента удалило её.
Сокращение динамических ссылок
Чтобы присоединиться к группе, пользователь должен быть приглашен через динамическую ссылку. Когда пользователь нажимает на ссылку, его перенаправляет в приложение Settle Up и ему предлагают вступить в группу. Ссылка генерируется локально в клиентском приложении и выглядит так:
https://abc123.app.goo.gl/?link=https://example.com/&apn=com.example.android&ibi=com.example.ios
Это довольно длинная и уродливая ссылка. Её возможно сократить, используя простой HTTP POST. Чтобы снова упростить клиентский код, это сокращение происходит на сервере:
Здесь есть два интересных момента. Первый – как мы проверяем, какая из ссылок не сокращена – просто смотрим, содержит ли она “?link”. Это, возможно, слишком просто, но это работает.
Другой связан с разными средами. Мы активно используем три среды – песочницу, альфа и живую среду. Конечная точка сокращения ссылок требует API ключ для нашего приложения (~среды) и, так как у нас их три, мы должны убедиться, что в каждом случае используется правильный ключ.
Чтобы достичь этого, мы используем Firebase Config. Установка этой конфигурации – это набор следующих команд, запущенных для каждой среды:
1 2 3 |
firebase use sandbox firebase functions:config:set local.apikey="<APIKEY>" firebase deploy --only functions |
Заключение
Как вы можете видеть, мы используем Firebase и Cloud Functions довольно активно. Мы используем их и в сценариях, которые я не упомянул: например, удаление целой группы (которое стирает данные из разных локаций, как и из хранилища Firebase).
В общем, мы довольны тем, как продвигается разработка, и прогрессом, который сделала команда Firebase. Functions очень полезны для нас по некоторым причинам:
- Они сильно упрощают разработку наших клиентских приложений: то, что нужно было писать несколько раз для каждой платформы, теперь можно написать один раз.
- Устранение различных крайних случаев – некоторая функциональность может страдать от них, если они происходят в клиентских приложениях, например, при потере соединения с интернетом.
- Наши клиентские приложения более безопасны – нам не нужно хранить ключи API разных сервисов в приложениях, а только на сервере.
- Легкая отправка пуш-уведомлений.
Безопасность и стабильность изменились с пре-бета стадии, и продукт стал довольно надежным при бета-релизе. Пока нет никаких важных проблем, которые могли бы вам помешать эффективно использовать Cloud Functions.