W3docs

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 проходит чётко определённый набор состояний. Понимание этих состояний — самое важное для того, чтобы избежать ошибок вида «почему всё ещё выполняется старый код?».

  1. Register — страница вызывает navigator.serviceWorker.register(). Браузер загружает скрипт.
  2. Install — событие install срабатывает один раз для каждой версии worker'а. Здесь производится предварительное кэширование файлов, необходимых приложению для работы в офлайне.
  3. Wait — если старый worker всё ещё управляет открытыми страницами, новый ждёт. Он не активируется, пока не будут закрыты все подконтрольные страницы, если только вы не вызовете self.skipWaiting().
  4. Activate — срабатывает событие activate. Здесь выполняется очистка кэшей из предыдущих версий.
  5. 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()). Описанный выше паттерн версионирования кэша обеспечивает чистоту обновлений:

  1. Увеличивайте CACHE_VERSION (например, 'v1''v2') при каждом изменении кэшированных ресурсов.
  2. Новая установка (install) записывает ресурсы в новый кэш.
  3. Новая активация (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, смотрите:

Практика

Практика
Каковы ключевые характеристики и функции JavaScript Service Workers?
Каковы ключевые характеристики и функции JavaScript Service Workers?
Was this page helpful?