Web Development

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

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

JavaMentor
JavaMentor

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

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

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

Но возможности подобных страниц этим не ограничиваются. С их помощью также можно предложить пользователю какое-нибудь увлекательное занятие, например разгадывание кроссворда. Именно так вам предстоит провести время на офлайн-странице блога разработчиков “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