Месяц назад я написал статью, объясняющую стек и цепочку инструментов, которые я собрал для создания крошечных адаптивных веб-приложений.
Выбранная мной технология решила следующие проблемы:
- Я хочу платить только за то, что использую (масштабирование до нуля)
- У меня не так много времени для обучения или создания (нет крутых кривых обучения без существенной экономии времени)
- У меня нет времени на техническое обслуживание (нет исправлений серверов, автоматизированное масштабирование)
- Я не хороший UI-дизайнер или фронтенд-инженер (дизайн-системы — это здорово).
Мой стек работает очень хорошо для приложения со стандартными операциями создания/получения/обновления/удаления, но недавно я столкнулся с требованием, требующим большей сложности на серверной части.
Проблема бизнеса
В своей оригинальной истории я использовал пример приложения, которое я создал, чтобы помочь с медитацией осознанности. Я создал приложение, чтобы обеспечить следующие функции:
- Неуправляемая медитация на время
- Возможность продолжить, когда таймер закончился, если хотите
- Возможность приостановить и возобновить сеанс
- Успокаивающий окружающий фоновый звук
- Периодический звонок, чтобы проверить, что я все еще в данный момент
- Случайные прерывистые, но актуальные звуки
- Автоматический дневник сеансов
Новое требование
Мое приложение работает очень хорошо, но я решил, что пришло время добавить некоторые статистические данные, чтобы обеспечить себе дополнительную поддержку. Обычная статистика мотивации для приложений для здоровья и фитнеса — показать самую длинную «полосу», с которой справился пользователь. В случае с приложением для медитации статистикой является максимальное количество дней подряд, в течение которых пользователь медитировал.
Мой текущий набор инструментов выглядит так:
Это двухуровневая архитектура, поэтому вся бизнес-логика находится на стороне клиента в браузере. Это нормально для текущих требований, но все меняется, когда мне нужно вычислить статистику. Я мог бы придерживаться этого стека и подсчитывать серии сессий в своем дневнике на стороне клиента, но сейчас я приближаюсь к 200 сессиям в моем дневнике. Если я перенесу это на пару лет вперед, то для отображения моей страницы статистики мне придется извлекать много данных из базы данных Firebase Realtime каждый раз, когда она загружается, и без необходимости повторять вычисления в браузере. Если однажды у меня будет много пользователей, то будет много данных, которые будут извлекаться без необходимости много времени, а это стоит денег.
Триггеры Firebase
Firebase удивительно богат функциональностью. Я объяснил, как я использовал функции Firebase (функции облачной платформы Google) в другой истории для создания REST API. Функции Firebase включают возможность регистрировать функции в качестве триггеров базы данных. Вместо запуска функции в ответ на HTTP-вызов функция может запускаться при создании, обновлении или удалении данных в базе данных. Код запускается с правами администратора и имеет доступ к запустившему его субъекту пользователя, а также к моментальному снимку данных, поэтому он идеально подходит для поддержания таблиц статистики в актуальном состоянии в ответ на изменения данных.
Я следовал примеру, используя триггер onUpdate()
, подробно описанный в документации. Я написал функцию, чтобы определить самую длинную полосу из списка записей дневника, и ее подключение было просто вопросом захвата записей из поля after
на снимке, предоставленном при обновлении, и записи результата обратно в родительский узел. Каждый раз, когда пользователь добавляет или удаляет сеанс медитации, статистика для этого пользователя автоматически пересчитывается. Поскольку статистика просто находится в базе данных, мое приложение может прочитать результат с помощью обычных вызовов API Firebase.
exports.countStreaks = functions.database.ref('diary/{userUid}/entries') .onUpdate(async (snapshot, context) => { const afterEntries = Object.entries(snapshot.after.val()); const processedEntries = []; for (const [key, value] of afterEntries) { const finishTime = DateTime.fromMillis(value.finishTime); const diaryEntry = {finishTime, totalSeconds: value.totalSeconds}; processedEntries.push(diaryEntry); } const stats = statscalculator.longestStreak(userTimezone, processedEntries); if(stats){ return snapshot.after.ref.parent.child('stats').set(stats); } });
Резюме
Ну это все. Я могу развернуть функцию с конвейером Github Actions CI/CD, как я описал в своей истории создания API с использованием функций Firebase. Я получаю масштабирование до нуля благодаря взиманию платы только за использование GCP, и мой код все еще находится в Node.js, поэтому не нужно изучать новые языки!
Бум.