Создание приложения для совместной работы с заметками в реальном времени с помощью Replicache

Источник: Telerik Blogs•

Создание приложения для совместной работы с заметками в реальном времени с помощью Replicache

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

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

У веб-приложений есть проблема с задержкой. Каждое взаимодействие вызывает «круговой» запрос. Ваше действие покидает браузер, достигает сервера, обрабатывается и возвращается в виде ответа. Только после этого экран обновляется.

Проблема в том, что эти «круговые» запросы не всегда надежны. Плохие сетевые условия или перегрузка сервера означают, что ответы приходят с опозданием или не приходят вовсе. Поэтому пользователи видят индикаторы загрузки или скелетные экраны.

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

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

Предварительные требования

Чтобы следовать этому руководству, предполагается, что у вас есть:

  • Базовые знания JavaScript/TypeScript
  • Понимание HTTP (GET/POST)
  • Установленный Node.js
  • Базовые знания SQL
  • Знакомство с React

Что такое Replicache?

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

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

Основные концепции

Прежде чем мы создадим наше приложение, давайте разберем основные концепции, благодаря которым работает Replicache.

Локальное хранилище

Локальное хранилище — это постоянная база данных, встроенная в браузер поверх IndexedDB. Это не память, которая исчезает после обновления страницы.

Оно работает как хранилище «ключ-значение», где каждый элемент данных имеет ключ (строка) и значение в формате JSON. Локальное хранилище является плоским, что означает, что все данные находятся вместе. При написании кода мы будем использовать префиксы для наших ключей, чтобы поддерживать порядок и удобство поиска. Это локальное хранилище также позволяет работать в автономном режиме; таким образом, изменения сохраняются локально и синхронизируются с сервером при восстановлении соединения.

Мутаторы

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

Мутатор — это просто функция, которая принимает WriteTransaction и некоторые аргументы. WriteTransaction — это ваш интерфейс для работы с локальным хранилищем. Для записи вам нужны только две операции: tx.set() для сохранения значения по ключу и tx.del() для удаления.

Когда вы вызываете мутатор, происходят две вещи. Во-первых, он немедленно записывает данные в локальное хранилище, поэтому интерфейс обновляется, и пользователь сразу видит изменения. Затем Replicache ставит мутацию в очередь и отправляет ее на сервер в фоновом режиме. Никаких состояний загрузки, синхронизация происходит незаметно.

Push (Отправка)

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

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

Poke (Сигнал)

Poke прост, но важен. Без него данные сохраняются в базе данных, но другие пользователи не узнают об изменениях. Им пришлось бы ждать следующего запланированного pull-запроса, который по умолчанию происходит раз в 60 секунд. Это не очень удобно для приложения для совместной работы.

Poke использует серверные события (SSE). Каждый браузер с открытым приложением поддерживает постоянное соединение с конечной точкой poke на сервере. Когда сервер завершает обработку push-запроса, он отправляет сигнал всем подключенным клиентам. Он не отправляет сами данные, только сигнал. Клиенты получают его и немедленно вызывают pull, чтобы получить актуальное состояние. Это просто и эффективно.

Pull (Получение)

Pull — это способ, которым Replicache получает актуальное состояние с сервера. Это происходит в трех ситуациях: при первой загрузке приложения, при получении клиентом сигнала poke и периодически в фоновом режиме в качестве страховки (по умолчанию каждые 60 секунд).

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

Перед применением патча Replicache проверяет наличие ожидающих мутаций в очереди. Если они есть, он сначала применяет данные сервера, а затем воспроизводит поверх них ожидающие мутации. Это помогает предотвратить потерю или перезапись локальных изменений входящими данными с сервера.

Настройка проекта

Мы создаем приложение для совместной работы с заметками в реальном времени, где пользователи могут создавать, редактировать, удалять заметки и реагировать на них с помощью эмодзи. Каждое внесенное изменение мгновенно синхронизируется во всех открытых вкладках браузера. На GIF-изображении ниже показано, что мы построим.

Мы разделим этот проект на две папки: server и client. Мы делаем это, потому что для них требуются разные зависимости.

Откройте терминал, перейдите в папку, где будет находиться проект, и выполните следующие команды:

Настройка сервера

Теперь давайте инициализируем и настроим сервер. Выполните следующую команду в терминале:

Теперь давайте установим необходимые зависимости:

Инициализация TypeScript:

Затем замените сгенерированный tsconfig.json на этот:

Настройка клиента

Теперь, когда сервер настроен, давайте создадим каркас клиента. Перейдите в каталог client:

Выполните следующую команду, чтобы создать React-приложение с помощью Vite:

Установите Replicache и его помощник для React:

Создание сервера

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

Создайте папку src внутри каталога server, именно там будут находиться все наши файлы:

Настройка базы данных

Создайте файл db.ts внутри папки src и добавьте в него следующее:

В приведенном выше коде мы создали три таблицы. Таблица notes хранит содержимое заметки, уникальный ID и временную метку. Таблица reactions хранит каждую реакцию-эмодзи с note_id, который ссылается на заметку, к которой она относится. Так реакция всегда «знает», к какой заметке она принадлежит. Таблица replicache_clients отслеживает последнюю мутацию, которую каждый клиент обработал на сервере. Мы подробно рассмотрим это в разделе продвинутых концепций.

Определение типов

Создайте файл types.ts в папке src и добавьте в него следующее:

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

Создание системы уведомлений

Создайте файл с именем poke.ts в каталоге src и добавьте в него следующее:

Мы поддерживаем Set всех открытых соединений с браузером. Set здесь идеален, потому что каждое соединение уникально, а удаление одного из них так же просто, как вызов delete(). В случае с массивом вам пришлось бы сначала найти его.

addClient добавляет браузер в Set при подключении, removeClient удаляет его при отключении, а poke циклически проходит по всем соединениям и отправляет сигнал по каждому из них.

Важно отметить, что poke не отправляет никаких фактических данных, только сигнал. Браузер получает его и немедленно вызывает pull для получения последних данных. Это делает систему чистой и эффективной.

Получение данных с сервера

Создайте файл с именем pull.ts в папке src. Это конечная точка, которую вызывает Replicache, когда ему нужны последние данные с сервера. Добавьте в него следующее:

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

Заметки используют префикс note/, а реакции — reaction/, поэтому мы можем различать их в плоском локальном хранилище. Без этих префиксов все находилось бы вместе, и не было бы способа отличить ключ заметки от ключа реакции.

Мы также объединяем оба массива в один плоский список, потому что Replicache ожидает один плоский массив инструкций, а не вложенные массивы. Ответ также включает cookie и lastMutationIDChanges, которые мы рассмотрим в разделе продвинутых концепций.

Обработка мутаций

Создайте файл с именем push.ts и добавьте в него следующее:

Тело запроса содержит массив мутаций. Каждая мутация имеет имя и объект args. Имя говорит нам, какую операцию запустить, а args содержит конкретные данные, необходимые этой операции.

Функция проходит по каждой мутации и проверяет ее имя. Для createNote мы вставляем новую заметку в таблицу notes. Для addReaction мы вставляем реакцию в таблицу reactions. Для updateNote мы обновляем содержимое существующей заметки, где ID совпадает. Для deleteNote мы выполняем два удаления: одно для заметки и одно для всех ее реакций. Мы делаем это, потому что не хотим, чтобы реакции указывали на заметку, которой больше не существует.

Сборка сервера

Создайте файл с именем index.ts в папке src, чтобы объединить все вместе:

В приведенном выше коде мы регистрируем маршруты push и pull и настраиваем конечную точку poke. Маршруты push и pull просты в том смысле, что они получают запрос, выполняют свою работу и отвечают.

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

Обратите внимание на заголовок keep-alive. Он говорит серверу не закрывать соединение после ответа. Пока в браузере открыто приложение, соединение остается активным. Когда браузер отключается или закрывает вкладку, req.on("close") срабатывает автоматически и удаляет это соединение из Set клиентов. Это предотвращает попытки отправить poke в соединение, которого больше не существует.

Создание клиента

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

Мы настроим:

  • Типы для согласования клиента и сервера
  • Мутаторы для обработки каждой операции записи
  • Экземпляр Replicache для подключения к серверу
  • Пользовательский интерфейс

Выполните следующую команду, чтобы перейти в папку клиента:

Создайте три новых файла в каталоге src:

Определение типов

И клиент, и сервер должны договориться об одной и той же структуре данных. Добавьте это в свой файл types.ts:

Это те же типы, что и на сервере. Используя общие определения типов, TypeScript обеспечивает согласованность между клиентом и сервером.

Определение мутаторов

Как мы рассматривали в разделе основных концепций, мутаторы — это единственный способ изменить данные в приложении Replicache. Обновите свой файл mutators.ts следующим образом:

Каждый мутатор получает WriteTransaction в качестве первого аргумента, который является вашим соединением с локальным хранилищем. Второй аргумент — это данные, необходимые для этой операции.

createNote сохраняет заметку в локальном хранилище под ключом note/{id}. addReaction делает то же самое для реакций под ключом reaction/{id}. deleteNote удаляет заметку с помощью tx.del().

С другой стороны, updateNote работает иначе. Он сначала считывает существующую заметку с помощью tx.get(), разворачивает ее и перезаписывает только поле содержимого. Все остальное остается прежним.

Важно отметить, что эти мутаторы не общаются с сервером напрямую. Они пишут только в локальное хранилище. Replicache обрабатывает их отправку на сервер отдельно, ставя каждую мутацию в очередь и отправляя всю очередь в конечную точку push.

Настройка экземпляра Replicache

Откройте файл replicache.ts и добавьте в него следующее:

Мы создаем экземпляр Replicache и экспортируем его как rep, чтобы его можно было использовать во всех наших компонентах. Имя — это уникальный идентификатор для локального хранилища. mutators сообщает Replicache, какие операции записи доступны. pushURL и pullURL указывают на конечные точки нашего сервера. Внизу мы открываем постоянное соединение с конечной точкой poke, используя EventSource. Когда сервер отправляет сигнал poke, срабатывает обработчик onmessage и вызывает rep.pull() для получения последних данных.

Создание пользовательского интерфейса

Именно здесь все становится видимым в браузере. Откройте App.tsx, очистите файл и добавьте эти импорты в начало:

Далее нам нужно подписаться на данные из локального хранилища.

Подписки

Добавьте это внутри вашей функции App:

В приведенном выше коде хук useSubscribe следит за локальным хранилищем и перерисовывает ваш компонент при изменении данных. tx.scan() фильтрует по префиксу. Помните, когда мы говорили, что нам нужно добавить префиксы к нашим ключам для легкого сканирования? Это то, что мы делаем здесь. Реакции также используют свой собственный префикс. Опция { default: [] } предотвращает сбой вашего компонента до загрузки данных.

Обработчики событий

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

handleUpdateNote вызывает мутатор updateNote и обрезает пробелы, чтобы предотвратить появление пустых заметок. handleAddReaction генерирует уникальный ID, фиксирует текущую временную метку и вызывает мутатор addReaction. handleDeleteNote удаляет заметку из хранилища. Массив emojis содержит реакции, которые могут выбирать пользователи.

Компоненты пользовательского интерфейса

Теперь мы видим все визуально. Мы отображаем сетку заметок, кнопку добавления заметки, редактируемое содержимое, реакции эмодзи и кнопку удаления. Поскольку у нас уже есть функции-обработчики, разметка проста:

Все готово. Если вы следовали инструкциям, ваше приложение должно правильно отображаться в браузере.

Сначала запустите сервер:

Затем откройте другой терминал для клиента:

Продвинутые концепции

Replicache обладает множеством продвинутых функций, но в этом руководстве мы сосредоточимся на cookie и lastMutationIDChanges. Мы уже упоминали их ранее в нашем коде, поэтому давайте кратко рассмотрим, для чего они нужны.

lastMutationIDChanges

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

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

Теперь давайте обновим файл pull.ts, чтобы увидеть это в действии. Замените переменную lastMutationIDChanges внутри вашей функции pull:

В этой статье мы установили cookie в значение null в нашем ответе pull, что означает, что сервер каждый раз отправляет все данные. Но есть проблема: когда вы открываете приложение в новом браузере, Replicache видит тот же null-cookie и думает, что ничего не изменилось. Он игнорирует массив patch, поэтому заметки не появятся.

Вот как мы можем это исправить. Вернитесь в файл pull.ts и измените cookie: null на cookie: Date.now(). Теперь каждый ответ pull имеет свежую временную метку. Replicache видит новый cookie и применяет данные патча.

Важно то, что временная метка всегда увеличивается. Date.now() всегда растет. Она никогда не уменьшается. Поэтому Replicache всегда видит число больше, чем раньше, что указывает на наличие свежих данных. Вот почему он применяет патч каждый раз.

Обновите свой финальный ответ следующим образом:

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

Заключение

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

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

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от Telerik