Первая страница без сети
После знакомства с жизненным циклом worker добавим первую автономную функцию. Сохраним XML-страницу и откроем её по тому же адресу при отсутствии сетевого ответа. Мы пока не загружаем всю библиотеку: маленький набор позволяет объяснить, какой ответ найден и почему он соответствует запросу.
В offline-lab старый путь /my/LINQ/linq_xml/level7/7_1.php уже выдаётся как готовый HTML. Для короткого первого состояния оформление находится внутри учебной страницы, поэтому у неё нет скрытой зависимости от внешнего CSS. Позднее выделим ресурсы и рассмотрим согласованный пакет. Замена настоящего ProfessorWeb этим примером не выполняется.
Успешная подготовка
Подготовим offline.html с сообщением о том, что нужный материал не сохранён и сейчас не получен из сети. Это обычный короткий HTML-документ нашего проекта. Он не утверждает, что запрошенная страница удалена. Для начальной установки нужны два ответа: запасная страница и XML-урок.
Ниже полный новый sw.js, который заменяет файл третьего урока. Установка получает оба документа и проверяет успешный HTTP-ответ и тип HTML. event.waitUntil связывает завершение этапа с подготовкой. Имена кешей принадлежат нашему приложению; мы не удаляем другие данные origin.
const CACHE = "pw-offline-v1";
const XML = "/my/LINQ/linq_xml/level7/7_1.php";
const FALLBACK = "/offline.html";
self.addEventListener("install", event => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE);
for (const path of [FALLBACK, XML]) {
const response = await fetch(path, {cache: "reload"});
const type = response.headers.get("content-type") ?? "";
if (!response.ok || !type.includes("text/html")) {
throw new Error("Required HTML is unavailable: " + path);
}
await cache.put(path, response);
}
})());
});
self.addEventListener("fetch", event => {
const request = event.request;
const url = new URL(request.url);
if (request.method !== "GET" || request.mode !== "navigate" ||
url.origin !== self.location.origin) return;
event.respondWith((async () => {
try {
return await fetch(request);
} catch {
const cache = await caches.open(CACHE);
const saved = await cache.match(request);
if (saved) return saved;
const fallback = await cache.match(FALLBACK);
const body = fallback ? await fallback.text() :
"<!doctype html><title>Недоступно</title><h1>Нет сохранённой страницы</h1>";
return new Response(body, {
status: 503,
headers: {"Content-Type": "text/html; charset=utf-8"}
});
}
})());
});
Проверка MIME не читает смысл всей статьи. Сервер способен вернуть HTML ошибки с 200, поэтому перед сохранением нужно отдельно сверить документ своего стенда. Это знакомая граница между кодом ответа и содержимым. Наш маленький исходник должен иметь ожидаемый заголовок и текст, а не только успешный сетевой статус.
Если подготовка второго документа завершится ошибкой, установка не станет успешной. При этом частичная запись может существовать в хранилище. Наличие такой записи само не означает полную готовность приложения. Пока у нас нет отдельного маркера пакета; позже введём его для сложного набора ресурсов. Не используйте один и тот же ключ новой версии, чтобы незаметно менять данные работающего выпуска.
Сначала сеть, затем точное совпадение
Стратегия примера называется network-first: сначала пробуется сетевой ответ. При сетевой ошибке worker ищет точное сохранённое совпадение. Если сервер вернул HTTP404, fetch обычно завершится ответом, а не отклонением Promise. Наш код вернёт этот ответ и не заменит его старым XML-документом. Использование Fetch.
Это различие важно для сохранения смысла URL. Отсутствие сети означает неизвестность текущего сервера. Настоящий 404 означает известный ответ. Один fallback для обоих случаев скрывал бы удаление материала или создавал ложную успешную страницу. В текущем примере сетевые ответы проходят без самостоятельного объявления, что библиотека сохранила их автоматически.
Cache API получает Request и возвращает Response. Мы используем точное сопоставление без глобального ignoreSearch. XML с дополнительным параметром может не совпасть с подготовленным адресом. Пока это честное ограничение маленького примера. Решение о допустимых параметрах принимается по реестру документа, а не путём игнорирования query string у всего приложения. Cache.match.
Полученный Response имеет поток тела. Во время установки мы передаём его кешу и больше не читаем. Если один ответ понадобится одновременно вернуть и сохранить, потребуется clone. Без этого один потребитель может использовать поток раньше другого. Так устройство объекта объясняет распространённую ошибку, когда программа правильно находит адрес, но не может прочитать тело.
Представьте два запроса: обычный путь XML и тот же путь с ?print=1. Первый уже входит в подготовленный набор. Второй может означать другую верстку с собственными ресурсами, поэтому простой кеш не обязан отвечать первым документом. Если позже выяснится, что единственный параметр содержит метку внешней кампании и не меняет страницу, можно отдельно построить разрешённое соответствие. Тогда программа убирает конкретную известную метку, а остальные параметры сохраняет. Такое решение описывает устройство нашего контента и не превращает любой похожий адрес в копию XML-урока.
Есть и другой важный вариант: читатель был онлайн, открыл Markdown и сразу отключил сеть. В текущем обработчике успешный сетевой ответ не записывается в кеш. Следовательно, посещение Markdown само не делает его сохранённым. Кнопка «Сохранить» следующего урока будет отдельным действием с наблюдаемым завершением. Эта граница позволяет объяснить состояние приложения без догадок о том, какие страницы случайно попали в обычный HTTP-кеш браузера.
Первое контролируемое открытие
В будущей практике сначала получите успешную регистрацию и подготовку, затем убедитесь, что новая навигация находится под controller. Если текущая вкладка ещё не управляется worker, выключение сети не проверит этот обработчик. Связь между active и controller рассматривалась в третьем уроке; регистрация сама по себе не гарантирует нужный путь запроса.
При отсутствии сети точный XML-адрес должен вернуть сохранённый учебный документ. Другой несохранённый URL получит запасное сообщение с 503. Этот код обозначает невозможность получить подходящий ответ в выбранном режиме, а не доказанное удаление. Сообщение должно объяснять следующий шаг: восстановить сеть или выбрать материал, который приложение действительно сохранило.
Наш start_url=/ пока не входит в подготовленный набор. Поэтому запуск установленного приложения без сети ещё не обещает полноценную главную. Это нормальная граница текущего состояния: мы научились возвращать одну главу. В следующем уроке расширим интерфейс выбора и его обязательные ресурсы, не объявляя все возможности PWA готовыми заранее.
Ограничение набора
Обработчик перехватывает только навигационные GET-запросы собственного origin. Он не кеширует внешние изображения, отправку формы или аналитические запросы. Для каждого нового типа ресурса потребуется своя политика. Универсальный обработчик любых запросов мог бы сохранить ошибку, личный ответ или несовместимые версии без объяснения.
В наших HTML-фрагментах нет приватного содержимого. Если проект позже добавит авторизацию, прежний подход нельзя автоматически распространить на личные страницы. Автономное хранение такого ответа требует модели доступа, выхода и очистки. Для статического учебника мы удерживаем маленький публичный набор и понятные условия.
Сохраните ожидаемые случаи отдельно: XML найден в кеше, несохранённая страница неизвестна без сети, настоящий сетевой 404 сохранён как ответ сервера, неправильный MIME останавливает подготовку. Эти состояния составят основу матрицы будущих проверок. Теперь можно перейти от автоматического сохранения одного примера к явному выбору глав самим читателем.