Перейти к содержанию

Update Flow — Полный код

Уровень: L3 (deep-dive) | ← Назад к PWA

Файл: frontend/src/hooks/useServiceWorker.ts

Хук управления жизненным циклом обновлений Service Worker.

import { useEffect, useState } from 'react';
import { registerSW } from 'virtual:pwa-register';

export function useServiceWorker() {
  const [needRefresh, setNeedRefresh] = useState(false);

  useEffect(() => {
    // Регистрация SW через vite-plugin-pwa virtual module
    const updateSW = registerSW({
      // Вызывается когда новая версия SW готова к активации
      onNeedRefresh() {
        setNeedRefresh(true);  // Показать пользователю промпт
      },
      // Вызывается когда app готов к offline-работе (первая установка)
      onOfflineReady() {
        console.log('[SW] App ready to work offline');
      },
    });

    // Cleanup: отменить регистрацию при unmount
    return () => {
      updateSW?.();
    };
  }, []);

  /** Применить обновление: перезагрузить страницу */
  const applyUpdate = () => {
    setNeedRefresh(false);
    window.location.reload();  // Новый SW активируется при reload
  };

  /** Отклонить обновление (пользователь решит позже) */
  const dismissUpdate = () => {
    setNeedRefresh(false);
  };

  return { needRefresh, applyUpdate, dismissUpdate };
}

Как пользователь узнаёт об обновлении

Поток:

sequenceDiagram
    participant Deploy as CI/CD Deploy
    participant CDN as CDN / Hosting
    participant SW as Service Worker
    participant Hook as useServiceWorker
    participant UI as Update Prompt

    Deploy->>CDN: Новый build (new sw.js)

    Note over SW: Браузер периодически проверяет sw.js
    SW->>CDN: GET /sw.js (byte-compare)
    CDN-->>SW: Новая версия!

    SW->>SW: install event (precache new assets)
    SW->>Hook: onNeedRefresh()
    Hook->>Hook: setNeedRefresh(true)
    Hook->>UI: Render prompt

    alt User clicks "Update"
        UI->>Hook: applyUpdate()
        Hook->>SW: window.location.reload()
        Note over SW: skipWaiting → activate → control
    else User dismisses
        UI->>Hook: dismissUpdate()
        Note over Hook: Обновление применится при следующем визите
    end

Диаграмма

Пользовательский опыт:

  1. Пользователь открывает приложение
  2. SW обнаруживает новую версию в фоне
  3. Появляется ненавязчивый промпт "Доступно обновление"
  4. Пользователь может:
  5. Обновить сейчасreload() → новая версия
  6. Позже → промпт закрывается, обновится при следующем визите

Интеграция в приложение

Хук используется в корневом компоненте App.tsx:

function App() {
  const { needRefresh, applyUpdate, dismissUpdate } = useServiceWorker();

  return (
    <>
      {/* ...роутинг... */}

      {needRefresh && (
        <UpdatePrompt
          onUpdate={applyUpdate}
          onDismiss={dismissUpdate}
        />
      )}
    </>
  );
}

Virtual Module: virtual:pwa-register

Этот модуль генерируется vite-plugin-pwa на этапе сборки. Он:

  1. Регистрирует Service Worker (navigator.serviceWorker.register())
  2. Настраивает registerType: 'autoUpdate' — автоматический skipWaiting + clients.claim
  3. Предоставляет callbacks:
  4. onNeedRefresh() — новый SW установлен, ждёт активации
  5. onOfflineReady() — все ресурсы закешированы
  6. onRegistered(registration) — SW успешно зарегистрирован
  7. onRegisterError(error) — ошибка регистрации

Почему autoUpdate?

Стратегия Поведение Для Plechiki
prompt Показать промпт, пользователь решает
autoUpdate Обновить автоматически при следующей загрузке

С autoUpdate новый SW активируется при reload. Комбинация с onNeedRefresh позволяет показать промпт, но обновление всё равно произойдёт при следующем визите если пользователь откажется.