Использование сервисных рабочих
В этой статье содержится информация о работе с сервисными рабочими, включая базовую архитектуру, регистрацию сервисного рабочего, процесс установки и активации нового сервисного рабочего, обновление вашего сервисного рабочего, кэширование и настраиваемые ответы, всё в контексте приложения с функциями работы офлайн.
Основы сервисных рабочих
Одна из ключевых проблем, с которой сталкиваются пользователи веб-приложений на протяжении многих лет, — это потеря подключения к сети. Даже самое лучшее веб-приложение окажется бесполезным, если его нельзя загрузить. Были различные попытки создания технологий для решения этой проблемы, и некоторые из проблем были решены. Но основной проблемой оставался недостаток хорошего механизма управления кэшированием активов и настраиваемыми сетевыми запросами.
Сервисные рабочие решают эти проблемы. С помощью сервисного рабочего вы можете настроить приложение на использование кэшированных активов в первую очередь, обеспечивая работу в автономном режиме, прежде чем получать данные из сети (часто называемое «offline first»). Это уже доступно в приложениях для настольных компьютеров, что является одной из основных причин, по которым такие приложения часто выбираются вместо веб-приложений.
Сервисный рабочий работает как прокси-сервер, позволяя изменять запросы и ответы, заменяя их элементами из собственного кэша.
Настройка для работы с сервисными рабочими
Сервисные рабочие по умолчанию включены во всех современных браузерах. Для выполнения кода, использующего сервисные рабочие, вам необходимо разместить код через HTTPS — сервисные рабочие ограничены работой по HTTPS по соображениям безопасности. Требуется сервер, поддерживающий HTTPS. Для проведения экспериментов можно использовать такие сервисы, как GitHub, Netlify, Vercel и т. д. Для облегчения локальной разработки браузеры также считают localhost безопасным источником.
Базовая архитектура
При использовании сервисных рабочих обычно выполняются следующие шаги для базовой настройки:
- Код сервисного рабочего извлекается и регистрируется с помощью
serviceWorkerContainer.register(). При успешном выполнении сервисный рабочий выполняется вServiceWorkerGlobalScope; это по сути особый тип контекста рабочего процесса, выполняющегося вне основного потока выполнения скрипта без доступа к DOM. Сервисный рабочий теперь готов обрабатывать события. - Происходит установка. Событие
installвсегда является первым событием, отправленным сервисному рабочему (это можно использовать для начала процесса заполнения IndexedDB и кеширования активов сайта). На этом этапе приложение готовится к использованию всего в автономном режиме. - После завершения обработчика
install, сервисный рабочий считается установленным. В этот момент предыдущая версия сервисного рабочего может быть активной и управлять открытыми страницами. Так как мы не хотим, чтобы одновременно выполнялись две разные версии одного и того же сервисного рабочего, новая версия еще не активна. - После того, как все страницы, управляемые старой версией сервисного рабочего, будут закрыты, можно убрать старую версию, и новый установленный сервисный рабочий получит событие
activate. Основное использованиеactivateзаключается в очистке ресурсов, используемых в предыдущих версиях сервисного рабочего. Новый сервисный рабочий может вызватьskipWaiting(), чтобы попросить немедленно активировать его, не дожидаясь закрытия открытых страниц. Новый сервисный рабочий сразу получитactivateи возьмет на себя управление всеми открытыми страницами. - После активации сервисный рабочий будет управлять страницами, но только теми, которые были открыты после успешного выполнения
register(). Другими словами, для фактического управления документами потребуется перезагрузка, так как документ начинает свою жизнь либо с наличием, либо без наличия сервисного рабочего и сохраняет это состояние на протяжении всего своего жизненного цикла. Чтобы переопределить это поведение по умолчанию и принять открытые страницы, сервисный рабочий может вызватьclients.claim(). - Всякий раз, когда извлекается новая версия сервисного рабочего, этот цикл повторяется, а остатки предыдущей версии удаляются во время активации новой версии.
Вот сводка доступных событий сервисного рабочего:
Демо
Для демонстрации самых основных операций регистрации и установки сервисного рабочего мы создали демо под названием простой сервисный рабочий, представляющий собой простую галерею изображений «Звёздных войн» Lego. Оно использует функцию с поддержкой обещаний для чтения данных изображения из объекта JSON и загрузки изображений с помощью fetch(), прежде чем отобразить изображения в строке вниз по странице. Сейчас всё статично. Оно также регистрирует, устанавливает и активирует сервисный рабочий.
Вы можете ознакомиться с исходным кодом на GitHub и рабочим примером простого сервисного рабочего.
Регистрация вашего рабочего
Первый блок кода в файле JavaScript нашего приложения — app.js — выглядит следующим образом. Это наш входной пункт для использования сервисных рабочих.
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
};
// …
registerServiceWorker();
- Блок
ifвыполняет проверку возможностей, чтобы убедиться в поддержке сервисных рабочих, прежде чем пытаться зарегистрировать их. - Затем мы используем функцию
ServiceWorkerContainer.register()для регистрации сервисного рабочего для этого сайта. Код сервисного рабочего находится в файле JavaScript, находящемся внутри нашего приложения (обратите внимание, что это URL файла относительно источника, а не файла JS, который на него ссылается). - Параметр
scopeявляется необязательным и может использоваться для указания подмножества вашего содержимого, которым будет управлять сервисный рабочий. В этом случае мы указали'/', что означает всё содержимое по адресу приложения. Если вы его пропустите, он будет по умолчанию равен этому значению, но мы указали его здесь для иллюстрации.
Это регистрирует сервисный рабочий, который работает в контексте рабочего процесса и, следовательно, не имеет доступа к DOM.
Один сервисный рабочий может управлять множеством страниц. Каждый раз, когда страница в вашем объеме загружается, сервисный рабочий устанавливается для этой страницы и управляет ею. Пожалуйста, имейте в виду, что вам нужно быть осторожными с глобальными переменными в скрипте сервисного рабочего: каждая страница не получает уникального рабочего.
Примечание: Отличная особенность сервисных рабочих заключается в том, что если вы используете проверку возможностей, как показано выше, браузеры, не поддерживающие сервисные рабочие, могут просто использовать ваше приложение онлайн в обычном режиме.
Почему мой сервисный рабочий не регистрируется?
Сервисный рабочий не регистрируется по одной из следующих причин:
- Ваше приложение не работает в безопасном контексте (через HTTPS).
- Неправильный путь к файлу сервисного рабочего. Путь должен быть относительным к источнику, а не к корню приложения. В нашем примере рабочий находится в
https://bncb2v.csb.app/sw.js, а корень приложения вhttps://bncb2v.csb.app/, поэтому сервисный рабочий должен быть указан как/sw.js. - Путь к вашему сервисному рабочему указывает на сервисный рабочий другого источника, отличного от вашего приложения.
- Регистрация сервисного рабочего содержит
scope-опцию более широкую, чем разрешено путем рабочего. Область действия сервисного рабочего по умолчанию — это каталог, где находится рабочий. Другими словами, если скриптsw.jsнаходится в/js/sw.js, он может управлять только URL-адресами в (или вложенными в) каталоге/js/по умолчанию. Область действия сервисного рабочего может быть расширена (или сужена) с помощью заголовкаService-Worker-Allowed. - Включены специфичные для браузера настройки, такие как блокировка всех файлов cookie, режим приватного просмотра, автоматическое удаление cookie при закрытии и т. д. См.
serviceWorker.register()совместимость с браузерами для получения дополнительной информации.
Установка и активация: заполнение кэша
После регистрации вашего сервисного работника браузер попытается установить, а затем активировать его для вашей страницы/сайта.
Событие install — это первое событие, которое срабатывает при установке или обновлении сервисного работника. Оно срабатывает только один раз, сразу после успешного завершения регистрации, и обычно используется для заполнения кэш-памяти браузера для офлайн-режима необходимыми ресурсами для работы приложения офлайн. Для этого мы используем API для хранения сервисного работника — cache — глобальный объект в сервисных работниках, который позволяет хранить ресурсы, полученные в ответах, используя запросы в качестве ключей. Этот API работает аналогично стандартному кэшу браузера, но он специфичен для вашего домена. Содержимое кэша сохраняется до его очистки.
Вот как наш сервисный работник обрабатывает событие install:
const addResourcesToCache = async (resources) => {
const cache = await caches.open("v1");
await cache.addAll(resources);
};
self.addEventListener("install", (event) => {
event.waitUntil(
addResourcesToCache([
"/",
"/index.html",
"/style.css",
"/app.js",
"/image-list.js",
"/star-wars-logo.jpg",
"/gallery/bountyHunters.jpg",
"/gallery/myLittleVader.jpg",
"/gallery/snowTroopers.jpg",
]),
);
});
- Здесь мы добавляем обработчик события
installк сервисному работнику (отсюдаself), а затем цепляем методExtendableEvent.waitUntil()к событию — это гарантирует, что сервисный работник не будет установлен, пока код внутриwaitUntil()не выполнится успешно. - Внутри
addResourcesToCache()мы используем методcaches.open()для создания нового кэша под названиемv1, который будет представлять собой версию 1 кэша ресурсов нашего сайта. Затем мы вызываем функциюaddAll()на созданном кэше, которая в качестве параметра принимает массив URL-адресов всех ресурсов, которые вы хотите кэшировать. URL-адреса относительны к расположению работника location. - Если обещание отклонено, установка завершается неудачей, и работник ничего не делает. Это нормально, так как вы можете исправить код и повторить попытку при следующей регистрации.
- После успешной установки сервисный работник активируется. Это не имеет особого значения при первой установке/активации сервисного работника, но это более важно при обновлении сервисного работника (см. раздел «Обновление вашего сервисного работника» ниже).
Примечание: API для хранения данных в браузере (localStorage) работает аналогично кэшу сервисного работника, но он синхронный, поэтому не допускается в сервисных работниках.
Примечание: IndexedDB может использоваться внутри сервисного работника для хранения данных, если это необходимо.
Настраиваемые ответы на запросы
Теперь, когда ресурсы сайта кэшированы, необходимо указать сервисным работникам, что им нужно делать с кэшированным содержимым. Это делается с помощью события fetch.
-
Событие
fetchсрабатывает каждый раз, когда извлекается любой ресурс, контролируемый сервисным работником, включая документы в указанном объеме, и любые ресурсы, на которые ссылаются эти документы (например, еслиindex.htmlделает междоменный запрос для вставки изображения, он все равно проходит через свой сервисный работник). -
Вы можете прикрепить обработчик события
fetchк сервисному работнику, а затем вызвать методrespondWith()на событии, чтобы перехватить HTTP-ответы и обновить их своим собственным содержимым.self.addEventListener("fetch", (event) => { event.respondWith(/* custom content goes here */); }); -
Можно начать с ответа ресурсом, URL которого соответствует URL-адресу сетевого запроса, в каждом случае:
self.addEventListener("fetch", (event) => { event.respondWith(caches.match(event.request)); });caches.match(event.request)позволяет нам сопоставлять каждый запрашиваемый из сети ресурс с эквивалентным ресурсом, доступным в кэше, если такой имеется. Сопоставление выполняется по URL-адресу и различным заголовкам, аналогично обычным HTTP-запросам.
Восстановление неудачных запросов
Таким образом, caches.match(event.request) отлично подходит, когда в кэше сервисного работника есть соответствие, но что делать в случаях, когда такого соответствия нет? Если мы не предоставим никакого варианта обработки ошибок, наше обещание будет выполнено с undefined, и мы ничего не получим.
После проверки ответа из кэша мы можем перейти к обычному сетевому запросу:
const cacheFirst = async (request) => {
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
return fetch(request);
};
self.addEventListener("fetch", (event) => {
event.respondWith(cacheFirst(event.request));
});
Если ресурсов нет в кэше, они запрашиваются из сети.
Используя более сложную стратегию, мы можем не только запросить ресурс из сети, но и сохранить его в кэше, чтобы в дальнейшем запросы на этот ресурс тоже можно было получить офлайн. Это означает, что если к галерее Звездных войн будут добавлены дополнительные изображения, наше приложение сможет автоматически их получить и кэшировать. Следующий фрагмент кода реализует такую стратегию:
const putInCache = async (request, response) => {
const cache = await caches.open("v1");
await cache.put(request, response);
};
const cacheFirst = async (request) => {
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
const responseFromNetwork = await fetch(request);
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
};
self.addEventListener("fetch", (event) => {
event.respondWith(cacheFirst(event.request));
});
Если URL-адрес запроса недоступен в кэше, мы запрашиваем ресурс из сетевого запроса с помощью await fetch(request). После этого мы помещаем копию ответа в кэш. Функция putInCache() использует caches.open('v1') и cache.put() для добавления ресурса в кэш. Исходный ответ возвращается браузеру, чтобы он был предоставлен странице, которая его запросила.
Клонирование ответа необходимо, потому что потоки запроса и ответа можно читать только один раз. Чтобы вернуть ответ браузеру и поместить его в кэш, нужно его клонировать. Таким образом, оригинал возвращается браузеру, а копия отправляется в кэш. Каждый из них читается один раз.
Необычным может показаться, что обещание, возвращаемое putInCache(), не ожидается. Но причина в том, что мы не хотим ждать, пока копия ответа не будет добавлена в кэш, прежде чем возвращать ответ.
Единственная проблема, которая у нас сейчас есть, заключается в том, что если запрос не соответствует ничего в кэше, и сеть недоступна, наш запрос все равно потерпит неудачу. Давайте предоставим по умолчанию резервное решение, чтобы в любом случае пользователь получил что-то:
const putInCache = async (request, response) => {
const cache = await caches.open("v1");
await cache.put(request, response);
};
const cacheFirst = async ({ request, fallbackUrl }) => {
// First try to get the resource from the cache
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
// Next try to get the resource from the network
try {
const responseFromNetwork = await fetch(request);
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
} catch (error) {
const fallbackResponse = await caches.match(fallbackUrl);
if (fallbackResponse) {
return fallbackResponse;
}
// when even the fallback response is not available,
// there is nothing we can do, but we must always
// return a Response object
return new Response("Network error happened", {
status: 408,
headers: { "Content-Type": "text/plain" },
});
}
};
self.addEventListener("fetch", (event) => {
event.respondWith(
cacheFirst({
request: event.request,
fallbackUrl: "/gallery/myLittleVader.jpg",
}),
);
});
Мы выбрали это резервное изображение, потому что единственные обновления, которые могут потерпеть неудачу, — это новые изображения, поскольку все остальное зависит от установки в обработчике события install , который мы видели ранее.
Предварительная загрузка навигации сервисного работника
Если функция предварительной загрузки навигации включена, она начинает загружать ресурсы, как только выполняется запрос, и параллельно с активацией сервисного работника. Это гарантирует, что загрузка начнется сразу при переходе на страницу, а не придется ждать активации сервисного работника. Эта задержка случается относительно редко, но она неизбежна, когда происходит, и может быть значительной.
Сначала функция должна быть включена во время активации сервисного работника, используя registration.navigationPreload.enable():
self.addEventListener("activate", (event) => {
event.waitUntil(self.registration?.navigationPreload.enable());
});
Затем используйте event.preloadResponse для ожидания завершения загрузки предварительно загруженного ресурса в обработчике события fetch.
Продолжая пример из предыдущих разделов, мы вставляем код для ожидания предварительно загруженного ресурса после проверки кэша и перед извлечением из сети, если это не удалось.
Новая последовательность действий:
- Проверка кэша
- Ожидание
event.preloadResponse, который передается какpreloadResponsePromiseв функциюcacheFirst(). Кэшировать результат, если он возвращается. - Если ни один из этих пунктов не определен, мы переходим к сети.
const addResourcesToCache = async (resources) => {
const cache = await caches.open("v1");
await cache.addAll(resources);
};
const putInCache = async (request, response) => {
const cache = await caches.open("v1");
await cache.put(request, response);
};
const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
// First try to get the resource from the cache
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
// Next try to use (and cache) the preloaded response, if it's there
const preloadResponse = await preloadResponsePromise;
if (preloadResponse) {
console.info("using preload response", preloadResponse);
putInCache(request, preloadResponse.clone());
return preloadResponse;
}
// Next try to get the resource from the network
try {
const responseFromNetwork = await fetch(request);
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
} catch (error) {
const fallbackResponse = await caches.match(fallbackUrl);
if (fallbackResponse) {
return fallbackResponse;
}
// when even the fallback response is not available,
// there is nothing we can do, but we must always
// return a Response object
return new Response("Network error happened", {
status: 408,
headers: { "Content-Type": "text/plain" },
});
}
};
// Enable navigation preload
const enableNavigationPreload = async () => {
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
};
self.addEventListener("activate", (event) => {
event.waitUntil(enableNavigationPreload());
});
self.addEventListener("install", (event) => {
event.waitUntil(
addResourcesToCache([
"/",
"/index.html",
"/style.css",
"/app.js",
"/image-list.js",
"/star-wars-logo.jpg",
"/gallery/bountyHunters.jpg",
"/gallery/myLittleVader.jpg",
"/gallery/snowTroopers.jpg",
]),
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
cacheFirst({
request: event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: "/gallery/myLittleVader.jpg",
}),
);
});
Обратите внимание, что в этом примере мы загружаем и кэшируем одни и те же данные для ресурса, независимо от того, загружается ли он «обычно» или предварительно загружается. Вы также можете выбрать загрузку и кэширование другого ресурса при предварительной загрузке. Для получения дополнительной информации см. NavigationPreloadManager > Настраиваемые ответы.
Обновление вашего сервисного работника
Если ваш сервисный работник был ранее установлен, но затем новая версия работника доступна при обновлении или загрузке страницы, новая версия устанавливается в фоновом режиме, но еще не активируется. Она активируется только тогда, когда больше нет загруженных страниц, которые все еще используют старый сервисный работник. Как только больше нет таких загруженных страниц, новый сервисный работник активируется.
Примечание: Можно обойти это, используя Clients.claim().
Вам потребуется обновить обработчик события install в новом сервисных работнике на что-то вроде этого (обратите внимание на новый номер версии):
const addResourcesToCache = async (resources) => {
const cache = await caches.open("v2");
await cache.addAll(resources);
};
self.addEventListener("install", (event) => {
event.waitUntil(
addResourcesToCache([
"/",
"/index.html",
"/style.css",
"/app.js",
"/image-list.js",
// …
// include other new resources for the new version…
]),
);
});
Пока сервисный работник устанавливается, предыдущая версия все еще отвечает за извлечения. Новая версия устанавливается в фоновом режиме. Мы используем новый кэш v2, поэтому предыдущий кэш v1 не нарушается.
Когда ни одна страница не использует предыдущую версию, новый работник активируется и начинает отвечать за извлечения.
Удаление старых кэшей
Как мы видели в предыдущем разделе, при обновлении сервисного работника до новой версии вы создадите новый кэш в его обработчике события install. Пока существуют открытые страницы, которые контролируются предыдущей версией работника, вам нужно сохранить оба кэша, потому что предыдущая версия нуждается в своей версии кэша. Вы можете использовать событие activate для удаления данных из предыдущих кэшей.
Обещания, переданные в waitUntil() , заблокируют другие события до завершения, поэтому вы можете быть уверены, что операция очистки будет завершена к моменту получения первого события fetch в новом сервисных работнике.
const deleteCache = async (key) => {
await caches.delete(key);
};
const deleteOldCaches = async () => {
const cacheKeepList = ["v2"];
const keyList = await caches.keys();
const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
await Promise.all(cachesToDelete.map(deleteCache));
};
self.addEventListener("activate", (event) => {
event.waitUntil(deleteOldCaches());
});
Инструменты разработчика
- Chrome
-
Firefox
- Кнопка «Забыть об этом сайте», доступная в настройках пользовательского интерфейса Firefox, может использоваться для очистки сервисных работников и их кэшей.
- Edge
См. также
- Обещания
- Использование веб-работников
-
Service-Worker-AllowedHTTP-заголовок
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers