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

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

Дружеское предостережение

Хотя настроить базового работника службы, настроенного на предварительное кэширование ресурсов для повышения производительности, относительно просто, заставить веб-приложение фактически функционировать в автономном режиме непросто. Нет серебряной пули; включение автономных функций требует довольно много усилий - гораздо больше, чем я предполагал, когда начинал это делать. Если вы ожидаете увидеть несколько API-интерфейсов и утилит, которые можно использовать, и быстро заставить что-то работать, вы можете быть разочарованы. Более того, API IndexedDB может быть крайне неудобным. Хотя в npm есть помощники, такие как idb и idb-keyval, оба от Джейка Арчибальда, чтобы помочь с этим, в этом посте будут использоваться только стандартные API и функции. Единственная причина этого заключается в том, чтобы помочь лучше понять основные инструменты, поэтому, если вы когда-нибудь решите попробовать это, вы лучше поймете, что происходит, и вам будет легче выбрать подходящих помощников. , и отлаживать при необходимости.

Тем не менее, у этой философии есть пределы. Получение работающего Service Worker'а для предварительного кеширования и обновления ваших ресурсов по мере необходимости было бы проектом сам по себе. К счастью, есть утилиты, которые могут помочь, а именно Workbox Google, который я буду использовать ниже. Если вам интересно, какой вид работы потребуется для создания такого сервис-воркера с нуля, ознакомьтесь с этим вопросом, который я задал в Stack Overflow несколько лет назад, на который Джефф Посник любезно ответил.

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

Обеспечение рендеринга вашего сайта в офлайн-режиме

Давайте сразу перейдем к делу и посмотрим на нашу конфигурацию Workbox.

Это приведет к тому, что все наши ресурсы веб-пакета будут предварительно кэшированы, за исключением некоторых форматов шрифтов, используемых старыми браузерами (они все равно будут загружаться и работать при необходимости, они просто не будут предварительно кэшироваться в сервис-воркере; на самом деле, эти браузеры, вероятно, не t в любом случае работники службы поддержки). Свойство navigateFallback сообщает рабочему полю, как обрабатывать любые запросы навигации, которые не были предварительно кэшированы, а importScripts сообщает нашему сгенерированному сервисному воркеру, что нужно импортировать этот конкретный файл; здесь мы разместим наш код автономной синхронизации.

Начать отвечать на запросы данных

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

Чтобы исправить это, давайте настроим маршрут Workbox, который проверяет, что запрашивается, и, в случае сбоя, удовлетворяет запрос данными из IndexedDB. Поскольку это приложение использует GraphQL для своих данных, это становится немного проще: все запросы будут проходить по одному и тому же пути, и мы можем просто проверить часть запроса URL-адреса. Конечно, если вы не используете GraphQL, вам нужно просто проверить URL-адрес каким-либо другим способом, чтобы выяснить, что загружать из IndexedDB.

Давайте посмотрим код

Давайте разберемся с этим. Мы импортируем тематический запрос и используем метод parseQueryString из пакета query-string, чтобы увидеть, что запрашивается. В случае сбоя сетевого запроса мы будем считать, что мы не в сети, и ответим данными IndexedDB, которые извлекает функция readTable. Наконец, мы не можем просто вернуть любую желаемую структуру данных; нам нужно точно соответствовать тому, что вернула бы наша онлайн-конечная точка (GraphQL для этого приложения), чтобы код нашего приложения по-прежнему работал.

Давайте возьмем эти кусочки по одной. Во-первых, метод readTable

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

Функция gqlResponse - в основном просто ведение домашнего хозяйства; он помещает наши результаты в формат, который GraphQL отправил бы обратно. Не позволяйте двойному синтаксису => вводить вас в заблуждение; это просто функция более высокого порядка или функция, возвращающая функцию.

const gqlResponse = (op, coll) => data => new Response(JSON.stringify({ data: { [op]: { [coll]: data } } }));

И теперь наши темы будут загружаться в автономном режиме

Синхронизация данных в IndexedDB

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

Чтобы наши данные синхронизировались, мы настроим другой маршрут для того же /graphql пути, но на этот раз мы будем прослушивать POST вместо GET, поскольку именно там будут происходить наши мутации. Предсказуемый характер наших ответов GraphQL позволяет легко увидеть, что было изменено, поэтому мы можем найти соответствующую запись в IndexedDB и синхронизировать изменения. Давайте рассмотрим его шаг за шагом. Вот код для обработки фактического запроса на изменение GraphQL.

Мы запускаем мутации GraphQL как обычно, так как ожидаем, что они будут выполняться только в режиме онлайн. response.clone() используется по необходимости, поскольку ответы на выборку могут быть использованы только один раз, а вызовы response.json() или передача event.responseWith или cache.put считаются потреблением. Кроме того, код вызывает syncResultsFor для синхронизации различных типов, которые могли быть изменены нашей мутацией GraphQL. Пойдем дальше.

Эта функция проходит через различные формы, в которых могут находиться наши результаты GraphQL, получает обновленные значения и синхронизируется с IndexedDB. Естественно, ваш код здесь может немного отличаться, в зависимости от того, как сконструирована ваша конечная точка GraphQL; этот был автоматически сгенерирован моим проектом mongo-graphql-starter.

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

Здесь мы ищем объект по его Mongo _id. Если он есть, мы его обновляем; если нет - вставляем. В любом случае к объекту применяется необязательное преобразование, чтобы при необходимости массировать данные - например, для дублирования строкового поля в качестве значения нижнего регистра для индексации, поскольку IndexedDB на момент написания не обеспечивает регистронезависимость индексы.

Функция deleteItem намного проще и выглядит так:

Ограничение функциональности приложения в автономном режиме

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

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

Этот раздел является одним из самых простых, поскольку онлайн- и офлайн-мероприятия упрощают его.

window.addEventListener(“offline”, () => store.dispatch({ type: IS_OFFLINE }));
window.addEventListener(“online”, () => store.dispatch({ type: IS_ONLINE }));

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

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

Более продвинутый поиск данных с IndexedDB

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

Подчеркну, этот код создан для скорости, не комфорта. Я написал это, чтобы избежать ненужного сканирования таблиц. Вполне вероятно, что в большинстве случаев это будет чрезмерная инженерия. Я написал этот код в качестве обучающего упражнения, для бессмысленного побочного проекта, для развлечения. Обязательно войдите в профиль, прежде чем писать что-либо подобное; как я уже сказал, существует множество библиотек IndexedDB, которые предоставляют дружественные API для поиска, разбиения на страницы и фильтрации данных. Скорее всего, они делают это, выгружая в память целые таблицы, но, как я уже сказал, это часто не имеет значения. Профилируйте, прежде чем принимать какие-либо решения.

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

Обычно СУБД предоставляют все это из коробки, поэтому для реализации чего-то подобного вы должны составить динамический запрос (избегая атак путем внедрения); однако с IndexedDB API гораздо более ограничен. Здесь мы можем только открыть курсор либо на хранилище объектов (таблицу), либо на индекс. Индексные курсоры дают нам возможность читать вперед или назад, а также возможность пропускать ряд записей. Эти функции позволяют нам реализовать сортировку в порядке возрастания и убывания и разбиение на страницы соответственно. Однако для фильтрации нам нужно будет вручную проверять каждый результат.

Вот как мы это реализуем. Поля сортировки в запросе GraphQL будут указывать, какой индекс открывать и в каком направлении читать. Если фильтрации нет, все просто: мы сразу пропустим соответствующее количество строк, а затем просто прочитаем столько элементов, чтобы заполнить нашу страницу. Если есть фильтрация, нам нужно проделать значительно больше работы, поскольку индексы IDB позволяют вам искать только определенное значение. Поэтому при фильтрации нам нужно будет обработать каждую запись в индексе, вручную пропустить соответствующее количество совпадений, а затем собрать следующее количество совпадений, чтобы заполнить нашу страницу.

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

Аргумент cursorSkip касается курсора и перескакивает через это количество записей. Это возможно при отсутствии фильтрации; если есть, аргумент skip указывает, сколько записей пропустить вручную на основе предиката фильтрации.

Подчеркну еще раз, что абстракции могут значительно упростить этот код. Единственное спасение в том, что здесь есть, - это перфоманс; вся таблица никогда не обрабатывается, за исключением маловероятного случая, когда пользователь просматривает всю страницу до последней страницы (или таблица настолько мала, что умещается на одной странице). Если объем ваших данных достаточно мал, чтобы это не имело значения, что, скорее всего, будет, то обратитесь к помощнику IndexedDB, чтобы упростить задачу. Всегда учитывайте и оценивайте, прежде чем принимать решение.

Удачного кодирования!

Дальнейшее чтение

Если вы хотите более глубоко изучить IndexedDB, Service Workers и / или PWA в целом, вам не лучше, чем Progressive Web Apps Тала Атера.