Service Workers: Магия офлайн-веба на реальных примерах

Service Workers: Магия офлайн-веба на реальных примерах

Представьте веб-страницу, которая загружается мгновенно, работает без интернета и может отправлять вам push-уведомления, как нативное приложение. Это не фантастика, а реальность, которую создают Service Workers — революционная технология, превращающая обычные сайты в Progressive Web Apps. В этой статье мы разберем конкретные примеры, от простого кэширования до сложных офлайн-стратегий, и покажем, как добавить эту магию в ваш проект.

Что такое Service Worker и зачем он нужен?

Service Worker — это скрипт, который браузер запускает в фоновом режиме, отдельно от веб-страницы. Он действует как прокси-сервер между вашим приложением, сетью и браузером. Его ключевые суперспособности:

  • Кэширование ресурсов: Сохранение HTML, CSS, JS, изображений для офлайн-работы.
  • Фоновая синхронизация: Отложенная отправка данных при появлении сети.
  • Push-уведомления: Вовлечение пользователей, даже когда сайт закрыт.
  • Управление сетевыми запросами: Перехват и модификация запросов/ответов.

Service Worker работает только по HTTPS (кроме localhost для разработки) и использует Promises для асинхронных операций. Это основа технологии Progressive Web Apps (PWA).

Пример 1: Базовый Service Worker для кэширования

Создадим простейший Service Worker, который кэширует основные ресурсы при установке.

Шаг 1: Регистрация (main.js)

На вашей основной странице добавьте:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW зарегистрирован!', reg))
    .catch(err => console.log('Ошибка регистрации SW:', err));
}

Шаг 2: Код Service Worker (sw.js)

const CACHE_NAME = 'my-cache-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/app.js',
  '/images/logo.png'
];

// Установка и кэширование ресурсов
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// Обработка запросов: сначала кэш, потом сеть
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

Пример 2: Продвинутая стратегия кэширования (Cache First, Network Fallback)

Для максимальной производительности используем стратегию «Сначала кэш, потом сеть» с обновлением кэша в фоне.

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        // Возвращаем кэш, если есть
        if (cachedResponse) {
          // Фоновая загрузка свежей версии для следующего раза
          fetchAndCache(event.request);
          return cachedResponse;
        }
        // Иначе загружаем из сети
        return fetchAndCache(event.request);
      })
  );
});

function fetchAndCache(request) {
  return fetch(request)
    .then(response => {
      // Клонируем ответ, т.к. поток можно прочитать только раз
      const responseClone = response.clone();
      caches.open(CACHE_NAME)
        .then(cache => cache.put(request, responseClone));
      return response;
    })
    .catch(error => {
      // Можно вернуть кастомную офлайн-страницу
      return caches.match('/offline.html');
    });
}

Всегда клонируйте Response перед помещением в кэш! Объект Response — это поток, который можно прочитать только один раз. Клонирование позволяет использовать ответ и для страницы, и для кэша.

Пример 3: Фоновая синхронизация данных

Допустим, пользователь оставил комментарий без сети. Service Worker может отложить отправку и выполнить её, когда соединение восстановится.

// На странице (при отправке формы)
if ('serviceWorker' in navigator && 'SyncManager' in window) {
  navigator.serviceWorker.ready
    .then(reg => reg.sync.register('sync-comments'))
    .catch(() => {
      // Fallback: сохраняем в IndexedDB для ручной синхронизации
    });
}

// В Service Worker
self.addEventListener('sync', event => {
  if (event.tag === 'sync-comments') {
    event.waitUntil(syncComments());
  }
});

async function syncComments() {
  const db = await openDB('CommentsDB', 1);
  const comments = await db.getAll('pending');
  for (const comment of comments) {
    try {
      await fetch('/api/comments', {
        method: 'POST',
        body: JSON.stringify(comment)
      });
      await db.delete('pending', comment.id);
    } catch (err) {
      console.error('Ошибка синхронизации:', err);
    }
  }
}

Пример 4: Push-уведомления

Интеграция с push-сервисами для вовлечения пользователей.

// Запрос разрешения и подписка на странице
Notification.requestPermission().then(permission => {
  if (permission === 'granted') {
    return navigator.serviceWorker.ready
      .then(reg => reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
      }));
  }
});

// Обработка push-событий в Service Worker
self.addEventListener('push', event => {
  const data = event.data ? event.data.json() : {};
  const options = {
    body: data.body || 'Новое уведомление!',
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    vibrate: [200, 100, 200]
  };
  event.waitUntil(
    self.registration.showNotification(data.title || 'Уведомление', options)
  );
});

Лучшие практики и подводные камни

  • Версионирование кэша: Всегда меняйте имя кэша (my-cache-v2) при обновлении ресурсов, чтобы старый кэш не мешал.
  • Очистка старых кэшей в событии activate:
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(key => key !== CACHE_NAME)
        .map(key => caches.delete(key)))
    )
  );
});
  • Не кэшируйте всё подряд: Исключите динамические данные (API-ответы), если они всегда должны быть свежими.
  • Тестируйте в Incognito: Чтобы избежать проблем с устаревшим Service Worker.

FAQ: Часто задаваемые вопросы

Service Worker работает во всех браузерах?

Да, все современные браузеры (Chrome, Firefox, Safari, Edge) поддерживают Service Workers. Для старых версий предусматривайте graceful degradation.

Как обновить Service Worker?

При изменении sw.js браузер обнаружит новую версию, установит её и активирует после закрытия всех вкладок с сайтом или принудительно через dev tools.

Service Worker потребляет много памяти?

Он останавливается, когда не используется. Браузеры эффективно управляют их жизненным циклом. Проблемы могут возникнуть только при утечках памяти в коде.

Можно ли использовать с SSR (Server-Side Rendering)?

Да, но осторожно. Кэшируйте только статические ресурсы или используйте стратегию Network First для HTML, чтобы контент был актуальным.

Как отлаживать Service Worker?

Используйте Chrome DevTools → вкладка Application → раздел Service Workers. Там можно смотреть кэш, принудительно обновлять, имитировать офлайн-режим.