Service Workers и кэширование

Основы Service Workers

Service Worker — это скрипт, работающий в фоне браузера независимо от веб-страницы. Он обеспечивает возможности для офлайн-доступа, фоновых синхронизаций, push-уведомлений и управления кэшированием ресурсов. В контексте Riot.js Service Worker можно использовать для ускорения загрузки компонентов, стилей и данных, что особенно важно для одностраничных приложений (SPA).

Service Worker регистрируется через JavaScript и управляется событием install, которое позволяет подготовить кэширование необходимых ресурсов:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(reg => console.log('Service Worker зарегистрирован', reg))
    .catch(err => console.error('Ошибка регистрации Service Worker', err));
}

Файл service-worker.js выполняет ключевые функции по кэшированию и обработке запросов.

Кэширование статических ресурсов

Статические ресурсы — это файлы, которые редко изменяются: HTML-шаблоны, CSS, JavaScript-компоненты Riot.js и изображения. Для их кэширования используется Cache API:

const CACHE_NAME = 'riot-app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/bundle.js',
  '/styles.css'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
  );
});
  • CACHE_NAME — уникальное имя кэша, которое позволяет управлять версиями.
  • STATIC_ASSETS — массив ресурсов для кэширования во время установки.

Использование event.waitUntil() гарантирует, что Service Worker не будет считаться установленным, пока кэш не завершит загрузку всех ресурсов.

Интерцептация сетевых запросов

Service Worker позволяет перехватывать сетевые запросы через событие fetch. Это позволяет отдавать ресурсы из кэша или обновлять их при необходимости:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request)
          .then(networkResponse => {
            return caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
          });
      })
  );
});

Особенности работы:

  • caches.match() проверяет наличие запроса в кэше.
  • Если ресурс отсутствует, выполняется сетевой запрос.
  • networkResponse.clone() необходим, так как поток ответа можно использовать только один раз. Один экземпляр возвращается браузеру, другой сохраняется в кэш.

Обновление кэша и управление версиями

Для корректной работы приложения требуется управление версиями кэша. Старые кэши удаляются при активации нового Service Worker:

self.addEventListener('activate', event => {
  const allowedCaches = [CACHE_NAME];
  event.waitUntil(
    caches.keys()
      .then(cacheNames => Promise.all(
        cacheNames.map(name => {
          if (!allowedCaches.includes(name)) {
            return caches.delete(name);
          }
        })
      ))
  );
});

Это позволяет предотвратить переполнение хранилища браузера устаревшими данными и гарантирует, что пользователи получают актуальные версии файлов Riot.js-компонентов.

Кэширование данных API

Для SPA на Riot.js важна работа с динамическими данными. Service Worker позволяет кэшировать ответы от API для офлайн-режима или ускорения повторных запросов:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      caches.open('api-cache')
        .then(cache => fetch(event.request)
          .then(response => {
            cache.put(event.request, response.clone());
            return response;
          })
          .catch(() => cache.match(event.request))
        )
    );
  }
});
  • Используется отдельный кэш для API (api-cache) для разделения статических ресурсов и динамических данных.
  • При отсутствии сети отдаётся закэшированный ответ.

Интеграция с Riot.js

В Riot.js компоненты загружаются динамически и могут содержать CSS и шаблоны. Использование Service Worker позволяет:

  • Кэшировать скомпилированные компоненты (bundle.js).
  • Отдавать локальные данные при офлайн-доступе.
  • Обновлять компоненты по мере появления новых версий на сервере.

Пример динамического запроса компонента с кэшем:

import { component } from 'riot';

fetch('/components/button.riot')
  .then(res => res.text())
  .then(source => {
    caches.open(CACHE_NAME).then(cache => cache.put('/components/button.riot', new Response(source)));
    component(source)(document.getElementById('app'));
  });

Стратегии кэширования

Существуют несколько стратегий кэширования, применимых в Riot.js:

  1. Cache First — сначала проверка кэша, затем сеть. Хорошо для статических файлов.
  2. Network First — сначала сеть, затем кэш. Подходит для динамических данных.
  3. Stale While Revalidate — отдаёт старую версию из кэша и параллельно обновляет её через сеть. Оптимально для баланса скорости и актуальности данных.

Выбор стратегии зависит от типа ресурсов и потребностей приложения.

Поддержка офлайн-режима

Service Worker обеспечивает работу приложения без подключения к интернету. Основной принцип — подготовка всех критических ресурсов в кэше на этапе install и обслуживание запросов через кэш:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
      .catch(() => caches.match('/offline.html'))
  );
});

offline.html отображается, если ни кэш, ни сеть не доступны.

Безопасность и особенности

  • Service Worker работает только по HTTPS.
  • Скрипт работает в отдельном потоке и не имеет прямого доступа к DOM.
  • Необходимо учитывать ограничения объема кэша в браузерах.
  • Версионирование и очистка устаревших кэшей критичны для стабильной работы.

Service Worker в связке с Riot.js позволяет создавать быстрые, надежные и офлайн-способные приложения, улучшая пользовательский опыт и снижая нагрузку на сервер.