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

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

Если мы не предусмотрели это обстоятельство, то можем увидеть сообщение об отсутствии подключения к интернету. 

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

Этот принцип позволяет сохранить последовательность пользовательского опыта во время сетевых сбоев и является предпочтительным вариантом реагирования на них нативных приложений.

Но возможности подобных страниц этим не ограничиваются. С их помощью также можно предложить пользователю какое-нибудь увлекательное занятие, например разгадывание кроссворда. Именно так вам предстоит провести время на офлайн-странице блога разработчиков “The Guardian” в момент потери соединения. 

Кроссворд на офлайн-странице The Guardian

Создание полезной офлайн-страницы для большинства веб-приложений  

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

Здесь в качестве примера приложения приводится FeedReader, программа для чтения формата RSS, благодаря которой пользователь может прочитать текстовый контент RSS-канала по URL-адресу, например вот такому: 

/feed/?url=https://ada.is/feed

Это приложение обрабатывается на сервере и возвращает все данные в формате HTML-страниц, которые кэшируются при помощи сервис-воркера. Если же ваше приложение использует для заполнения страниц на стороне клиента формат JSON, то этот метод по-прежнему будет работать при условии кэширования и JSON-ответов, и отображающих их страниц.

Этот шаблон применяется во многих веб-приложениях и будет работать до тех пор, пока есть кэшированные страницы.

Шаг 1. Предварительное кэширование офлайн-страницы 

Прежде всего, нам нужно сохранить офлайн-страницу при запуске приложения. С этой целью при старте были кэшированы HTML-файл /offline/ и его ресурсы /offline.js через заполнение кэша во время события install, выполняемого сервис-воркером. 

const CACHE_NAME = "DENORSS-v1.0.0";

self.addEventListener("install", (event) => {
 event.waitUntil(
  caches
   .open(CACHE_NAME)
   .then((cache) =>
    cache.addAll(["/", "/offline/", "/offline.js"])
   )
   .then(self.skipWaiting())
 );
});

Шаг 2. Показ офлайн-страницы 

Далее, когда пользователь пытается перейти на страницу, которой у нас нет, мы можем показать кэшированную страницу /offline/

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

// Попытаться отобразить офлайн-страницу при навигации 
if (event.request.mode === "navigate") {
    const offlinePage = await caches.match("/offline/");
    if (offlinePage) return offlinePage;
}

Шаг 3. Получение списка кэшированных страниц 

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

Сначала необходимо открыть кэши веб-приложений для поиска страниц, к которым нужно получить доступ:

const cacheKeys = await window.caches.keys();
const caches = await Promise.all(
 cacheKeys.map((cacheName) => window.caches.open(cacheName))
);

Так мы получаем массив кэшей. 

После этого требуется найти все кэшированные страницы из этого массива. С помощью cache.matchAll и ignoreSearch: true получим все результаты кэша в конечной точке /feed/.

const results = await Promise.all(
  caches.map((cache) =>
   cache.matchAll("/feed/", {
    ignoreSearch: true,
   })
  )
 );

Я просмотрела лишь конечную точку /feed/, поскольку, по моим ощущениям, такие страницы, как /search/ с результатами поиска или /404.html с сообщениями об ошибках, не будут представлять практической ценности для пользователя. Что касается главных страниц, таких как домашняя страница /, то они уже привязаны ссылками в навигационной панели. 

Инструкция results возвращает массив массивов, отражая результаты, полученные из каждого кэша. Далее мы произведем уплощение и обработаем каждый кэшированный ответ.

results.flat().forEach(async (response) => {
  // Здесь размещается код 
});

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

const params = new URLSearchParams(new URL(response.url).search);

const urlParam = params.get('url');

if (!urlParam) return;

Без параметра запроса url страница не представляет интереса, поэтому мы ее не показываем. 

Шаг 4. Отображение списка 

На данном этапе мы располагаем URL-адресами страниц и необработанными параметрами запросов, но в таком виде они могут не понравиться пользователю. Можно показать более привлекательные ярлыки, если обратиться к самому кэшированному содержимому.

Для извлечения данных из ответа нам необходимо получить его текст:  

const dataAsString = await response.text();

Если ваши данные хранятся в формате JSON, то JSON.parse будет достаточно для извлечения любой интересной информации, например заголовка хорошей страницы. 

const data = JSON.parse(dataAsString);

const title = data.title;

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

В следующем примере прочитаем текст в теге <title>. Другими подходящими элементами для запроса были бы <h1> или <h2>, позволяющие получить первый заголовок в документе. 

const html = document
  .createRange()
  .createContextualFragment(dataAsString);

const title = html
 .querySelector("title")
 .textContent.trim();

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

el.insertAdjacentHTML(
  "beforeend",
  `<li><a href="${response.url}">${title}</a></li>`
);

Анимация ниже отражает принцип работы нашей офлайн-страницы и был создан при помощи Chrome, имитирующего отсутствие сетевого соединения. 

Благодарю за внимание и надеюсь, что материал статьи будет вам полезен!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Ada Rose Cannon: Making a useful ‘offline’ page for your web app

Предыдущая статьяИмпорт в Python
Следующая статьяИмпорт в Python: часть 2