Service worker и офлайн — Примеры
Пример 1 — Одна стратегия кэширования для всего
Service worker отвечает на каждый запрос сначала из кэша. Пользователи видят цены и остатки, которым уже несколько дней, и жалуются, что сайт врёт. Объясните, что предполагает такая единственная стратегия, и выберите стратегию отдельно для каждого маршрута.
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((hit) => hit || fetch(event.request)),
);
});
Решение
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/api/")) {
event.respondWith(fetch(event.request)); // network-first: обязан быть свежим
return;
}
event.respondWith(
caches.match(event.request).then((hit) => hit || fetch(event.request)),
);
});
Cache-first — это утверждение, что ресурс не изменится: верное для хешированного файла и неверное для полезной нагрузки API. Значит, применив её повсюду, вы вписываете в воркер допущение, которого его автор не делал. Стратегия принадлежит ресурсу: хешированному файлу — cache-first, нагрузке, которая обязана быть свежей, — network-first, а значение, дорогое в вычислении, но терпящее лёгкую устарелость, отдайте под stale-while-revalidate. Решить, что где, маршрут за маршрутом, и составляет основную работу; кэш без срока жизни и без инвалидации — баг с длинным запалом, и его симптом — жалоба пользователя, а не строка в журнале.
Пример 2 — Имя кэша, которое никогда не меняется
Каждое развёртывание пишет новые ресурсы в один и тот же кэш. Пользователи работают на смеси двух релизов, и единственное известное лекарство — очистка данных сайта. Скажите, что релиз должен делать с кэшем, и версионируйте его.
self.addEventListener("install", (event) => {
event.waitUntil(caches.open("app").then((c) => c.addAll(["/", "/app.js", "/app.css"])));
});
Решение
const VERSION = "v7";
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(VERSION).then((c) => c.addAll(["/", "/app.js", "/app.css"])));
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((name) => name !== VERSION).map((name) => caches.delete(name))),
),
);
});
Кэш — это контейнер, и одно и то же имя от релиза к релизу означает, что файлы предыдущей сборки остаются доступными и смешиваются с текущими. Симптом поэтому приходит и уходит, а не воспроизводится стабильно. Имя кэша по сборке и удаление всех остальных имён при активации даёт каждому релизу чистое поколение и убирает старое. Причём удаление обязано происходить в activate, а не в install, потому что старый воркер продолжает обслуживать страницы, пока новый не возьмёт управление. Кэширование — ещё и место, где уместно решение о приватности: ответы с аутентификацией и всё пользовательское должны оставаться вне кэша, иначе их прочтёт следующий человек за устройством.