Service Workers
Изучите Service Workers в JavaScript: жизненный цикл, регистрация, стратегии кэширования, поддержка офлайн-режима и версионирование кэша.
Service Workers: создание мощных офлайн-приложений
Service Worker — это скрипт, который браузер выполняет в фоновом режиме, отдельно от веб-страницы, без прямого доступа к DOM. Он находится между вашим веб-приложением и сетью, выступая в роли программируемого прокси: каждый запрос, отправляемый страницей, можно перехватить, проверить, обслужить из кэша или переписать до того, как он достигнет сервера.
Эта единственная возможность открывает доступ к функциям, которых пользователи теперь ожидают от современных веб-приложений: работа в офлайн-режиме, практически мгновенные повторные загрузки, фоновая синхронизация данных и push-уведомления. Service Workers — это движок Progressive Web Apps (PWA).
В этой главе объясняется, что такое Service Workers, какой жизненный цикл они проходят, как зарегистрировать один из них, какие существуют стратегии кэширования и как выпускать обновления, не отдавая устаревшие файлы. API Service Worker полностью построен на Promises, поэтому пригодится рабочее знание async/await и Fetch API.
Что такое Service Worker?
Service Worker — это разновидность web worker: файл JavaScript, выполняющийся в собственном потоке, независимо от зарегистрировавшей его страницы. Поскольку он работает вне основного потока, он не может блокировать интерфейс, но также не имеет доступа к DOM — взаимодействие со страницами происходит через события и сообщения.
Ключевые особенности, отличающие его от обычного скрипта страницы:
- Он управляется событиями. Браузер запускает его, когда появляется работа (входящий
fetch,push,sync), и может завершить при простое. Никогда не рассчитывайте, что глобальное состояние сохраняется между событиями. - У него есть жизненный цикл. Service Worker устанавливается, активируется, и лишь затем управляет страницами. Обновления подчиняются строгим правилам, чтобы пользователи никогда не получали частично обновлённое приложение.
- Он ограничен областью видимости (scope). Worker может перехватывать запросы только в пределах своей области видимости — по умолчанию это каталог, в котором находится скрипт.
- Требует безопасного контекста. Service Workers работают только через HTTPS (или
localhostв процессе разработки), поскольку скрипт, способный перезаписывать любые ответы, представляет серьёзную угрозу безопасности.
Зачем использовать Service Workers?
| Преимущество | Что даёт |
|---|---|
| Офлайн-поддержка | Кэширование оболочки приложения и критически важных ресурсов, чтобы оно загружалось без сетевого подключения. |
| Производительность | Повторные посещения обслуживаются из локального кэша, устраняя сетевые запросы и сокращая время загрузки. |
| Фоновая синхронизация | Откладывать неудачные запросы (например, отправленный комментарий) и автоматически повторять их при восстановлении соединения. |
| Push-уведомления | Получать и отображать сообщения от сервера даже когда ни одной вкладки не открыто. |
| Полный контроль над запросами | Решать для каждого запроса, использовать ли кэш, сеть или собственную логику. |
Жизненный цикл Service Worker
Service Worker проходит чётко определённый набор состояний. Понимание этих состояний — самое важное для того, чтобы избежать ошибок вида «почему всё ещё выполняется старый код?».
- Register — страница вызывает
navigator.serviceWorker.register(). Браузер загружает скрипт. - Install — событие
installсрабатывает один раз для каждой версии worker'а. Здесь производится предварительное кэширование файлов, необходимых приложению для работы в офлайне. - Wait — если старый worker всё ещё управляет открытыми страницами, новый ждёт. Он не активируется, пока не будут закрыты все подконтрольные страницы, если только вы не вызовете
self.skipWaiting(). - Activate — срабатывает событие
activate. Здесь выполняется очистка кэшей из предыдущих версий. - Control / Fetch — после активации worker перехватывает события
fetchдля страниц в своей области видимости.
register → install → (waiting) → activate → fetch / push / sync ...Два метода управляют этим процессом:
self.skipWaiting()(вinstall) указывает новому worker'у активироваться немедленно, не дожидаясь закрытия страниц.self.clients.claim()(вactivate) позволяет активному worker'у взять под контроль уже открытые страницы, а не только те, что будут загружены после активации.
Зачем существует фаза ожидания: она гарантирует, что в течение всего времени жизни страницы ею управляет единственная версия кода — так старый HTML никогда не смешается с заново закэшированными скриптами. Используйте
skipWaiting()осознанно, поскольку это может подменить управляющий worker прямо во время активной сессии пользователя.
Ограничения, которые нужно учитывать
- Только HTTPS или
localhost. Страницы с незащищённым соединением не могут зарегистрировать worker. - Область видимости ограничивает перехват. Worker по адресу
/app/sw.jsуправляет/app/и вложенными путями — не всем origin'ом. Чтобы контролировать всё, разместите скрипт в корне сайта. - Нет доступа к DOM. Обновляйте страницу через передачу сообщений или путём чтения страницей данных из кэшей.
- Worker может быть завершён в любой момент. Всё, что должно сохраняться, храните в Cache Storage, IndexedDB или Web Storage — не в глобальных переменных worker'а.
Шаг 1 — Регистрация Service Worker
В JavaScript вашей страницы зарегистрируйте скрипт с помощью navigator.serviceWorker.register(). Всегда проверяйте поддержку заранее и регистрируйте worker после загрузки страницы, чтобы он не конкурировал с первоначальным рендерингом:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered, scope:', registration.scope);
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});
});
}Вызов register() возвращает Promise, который разрешается объектом ServiceWorkerRegistration. Его свойство scope сообщает, какими URL управляет данный worker.
Шаг 2 — Написание скрипта Service Worker
Создайте отдельный файл (здесь — sw.js) для самого worker'а. Внутри него обрабатываются события жизненного цикла и принимается решение о том, как обслуживать запросы. Пример ниже предварительно кэширует оболочку приложения при установке, очищает старые кэши при активации и применяет стратегию «сначала кэш» с офлайн-запасным вариантом:
const CACHE_VERSION = 'v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/offline.html'];
// Install: pre-cache the app shell.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting(); // activate this version immediately
});
// Activate: remove caches from previous versions.
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_VERSION)
.map((key) => caches.delete(key))
)
)
.then(() => self.clients.claim()) // take control of open pages
);
});
// Fetch: serve from cache, fall back to network, then to the offline page.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return (
cached ||
fetch(event.request).catch(() => caches.match('/offline.html'))
);
})
);
});Несколько важных моментов:
event.waitUntil(promise)удерживает worker в рабочем состоянии до тех пор, пока Promise не завершится, чтобы браузер не прервал его в середине установки или активации.event.respondWith(promise)— это то, как вы отвечаете на событиеfetch— возвращаетеResponse(из кэша) или Promise, который разрешается им.self.skipWaiting()принуждает новую версию активироваться без ожидания закрытия старых страниц. В сочетании сclients.claim()новый worker берёт управление немедленно. Удобно при разработке; в продакшне используйте осторожно, поскольку смена управляющего worker'а в середине сессии может прервать работу активных пользователей.
Шаг 3 — Worker берёт управление
После завершения установки и активации worker управляет страницами в своей области видимости, а его обработчик fetch перехватывает их запросы. Обратите внимание: первая загрузка страницы не проходит через worker — в это время он устанавливается. Начиная со второй загрузки, запросы проходят через ваш обработчик fetch.
Распространённые стратегии кэширования
Единственно правильной стратегии кэширования не существует — стратегию выбирают для каждого типа ресурсов в зависимости от того, насколько актуальными должны быть данные.
| Стратегия | Как работает | Лучше всего для |
|---|---|---|
| Cache first (сначала кэш) | Возвращает кэшированную копию; обращается к сети только при отсутствии в кэше. | Статические ресурсы, которые редко меняются (CSS, шрифты, оболочка приложения). |
| Network first (сначала сеть) | Пробует сеть; при сбое обращается к кэшу. | Часто обновляемый контент (ответы API, новостные ленты). |
| Stale-while-revalidate | Немедленно отдаёт кэшированную копию, а затем в фоне загружает свежую для следующего раза. | Ресурсы, где скорость важнее абсолютной актуальности (аватары, миниатюры). |
Обработчик network-first выглядит так:
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache a copy for offline use, then return the fresh response.
const copy = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, copy));
return response;
})
.catch(() => caches.match(event.request))
);
});Совет: Тело
Responseможно прочитать только один раз, поэтому необходимо вызватьclone()перед тем, как одновременно кэшировать и возвращать ответ.
Обновление Service Worker
Когда вы изменяете sw.js, браузер обнаруживает разницу в байтах, загружает новый файл и запускает его событие install. Затем новый worker ожидает (если только вы не вызываете skipWaiting()). Описанный выше паттерн версионирования кэша обеспечивает чистоту обновлений:
- Увеличивайте
CACHE_VERSION(например,'v1'→'v2') при каждом изменении кэшированных ресурсов. - Новая установка (
install) записывает ресурсы в новый кэш. - Новая активация (
activate) удаляет все кэши, ключи которых не соответствуют текущей версии, вытесняя устаревшие файлы.
Это гарантирует, что пользователи никогда не получат смешение старых и новых ресурсов после деплоя.
Реальный пример: уведомления о статусе подключения
В этом примере демонстрируется функция, широко используемая во многих современных сайтах и приложениях, таких как стриминговые сервисы Netflix или облачные приложения Google Docs, для информирования пользователей о статусе их подключения. Уведомляя пользователей об отсутствии сети, эти платформы улучшают пользовательский опыт, гарантируя, что они знают о возможных проблемах с синхронизацией данных или прерываниях стриминга. Этот пример сосредоточен на интеграции пользовательского интерфейса в основном потоке, тогда как скрипт Service Worker остаётся таким же, как в предыдущем примере.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Connectivity Notifier</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
#status {
padding: 10px;
border-radius: 5px;
color: #fff;
font-size: 24px;
}
.online {
background-color: #4caf50;
animation: blinker 1s linear infinite;
}
.offline {
background-color: #f44336;
animation: blinker 1s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0.5;
}
}
</style>
</head>
<body>
<h1>Connectivity Notifier</h1>
<p id="status" class="offline">Checking connectivity...</p>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(function () {
console.log("Service Worker Registered");
});
window.addEventListener('online', () => {
const statusElement = document.getElementById("status");
statusElement.textContent = "Online";
statusElement.className = "online";
});
window.addEventListener('offline', () => {
const statusElement = document.getElementById("status");
statusElement.textContent = "Offline";
statusElement.className = "offline";
});
}
</script>
</body>
</html>Пояснение:
- Проверка подключения: главная страница прослушивает события
onlineиofflineна объектеwindowи немедленно обновляет интерфейс, избегая ненадёжного подхода с опросом. - Обратная связь с пользователем: страница отображает текущий статус подключения, помогая пользователям понять, как интегрировать фоновые возможности с отзывчивым интерфейсом.
- Чистота кода: мёртвый обработчик
navigator.serviceWorker.onmessageбыл удалён, так как скрипт Service Worker не отправляет никаких сообщений.
Заключение
Service Workers превращают браузер в программируемый сетевой прокси, делая возможным создание быстрых, отказоустойчивых приложений, работающих в офлайн-режиме. Ключевые аспекты — понимание жизненного цикла (install → wait → activate → fetch), выбор стратегии кэширования, подходящей для каждого ресурса, и использование версионирования кэша для чистого развёртывания обновлений.
Чтобы глубже изучить строительные блоки, на которых основаны Service Workers, смотрите:
- Promise и async/await — весь API Service Worker основан на Promise.
- Fetch API — тот же
fetch(), который вы перехватываете в worker'е. - Storage API и localStorage & sessionStorage — где хранить данные, которыми управляет worker.
- Цикл событий: микрозадачи и макрозадачи — как worker планирует свою событийно-ориентированную работу.