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

Service Worker Config — Полный код

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

Файл: frontend/vite.config.ts

Полная конфигурация PWA через vite-plugin-pwa (Workbox).

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      // ─── Тип регистрации ───────────────────────────────────────────
      // 'autoUpdate' — SW обновляется автоматически при новом deploy
      registerType: 'autoUpdate',

      // ─── Precache: статические ресурсы ─────────────────────────────
      // Эти файлы будут включены в precache manifest
      includeAssets: ['icons/icon-192.png', 'icons/icon-512.png', 'icons/apple-touch-icon-180.png'],

      // ─── Web App Manifest ──────────────────────────────────────────
      manifest: {
        name: 'Плечики — цифровой гардероб',
        short_name: 'Плечики',
        description: 'Оцифруйте гардероб, собирайте образы, делитесь с друзьями',
        theme_color: '#D18A62',        // Цвет шапки браузера (warm accent)
        background_color: '#F7F4EF',   // Фон splash screen
        display: 'standalone',          // Без browser chrome
        start_url: '/',
        icons: [
          {
            src: '/icons/icon-192.png',
            sizes: '192x192',
            type: 'image/png',
          },
          {
            src: '/icons/icon-512.png',
            sizes: '512x512',
            type: 'image/png',
          },
          {
            src: '/icons/icon-512-maskable.png',
            sizes: '512x512',
            type: 'image/png',
            purpose: 'maskable',  // Адаптивная иконка для Android
          },
        ],
      },

      // ─── Workbox: Runtime Caching ──────────────────────────────────
      workbox: {
        // Precache: все статические ресурсы сборки
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],

        runtimeCaching: [
          // 1. Reference API — StaleWhileRevalidate
          // Мгновенный ответ из кеша + фоновое обновление
          {
            urlPattern: /\/api\/reference\/.*/,
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'reference-api',
              expiration: {
                maxEntries: 20,
                maxAgeSeconds: 7 * 24 * 60 * 60,  // 7 дней
              },
            },
          },

          // 2. MinIO Media — CacheFirst
          // Фото вещей и образов. Не меняются после загрузки.
          {
            urlPattern: /^https:\/\/minio-api\.plechiki\.ru\.dmitriy\.space\/.*/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'media-minio',
              expiration: {
                maxEntries: 2000,                    // До 2000 фото offline
                maxAgeSeconds: 30 * 24 * 60 * 60,   // 30 дней
              },
              cacheableResponse: { statuses: [200] }, // Кешировать только 200
            },
          },

          // 3. Media Service — CacheFirst
          // Отдельный сервис для resized изображений
          {
            urlPattern: /^https:\/\/media\.plechiki\.ru\.dmitriy\.space\/.*/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'media-service',
              expiration: {
                maxEntries: 2000,
                maxAgeSeconds: 30 * 24 * 60 * 60,
              },
              cacheableResponse: { statuses: [200] },
            },
          },

          // 4. Статические изображения — CacheFirst
          {
            urlPattern: /\.(png|jpg|jpeg|webp|svg)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'images',
              expiration: {
                maxEntries: 1000,
                maxAgeSeconds: 7 * 24 * 60 * 60,
              },
            },
          },

          // 5. Шрифты — CacheFirst (очень долгий TTL)
          {
            urlPattern: /\.(woff2?|ttf)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'fonts',
              expiration: {
                maxAgeSeconds: 30 * 24 * 60 * 60,
              },
            },
          },
        ],
      },

      // ─── Dev Options ───────────────────────────────────────────────
      // SW активен в dev-режиме для тестирования offline
      devOptions: {
        enabled: true,
      },
    }),
  ],

  // ─── Dev Server Proxy ──────────────────────────────────────────────
  server: {
    proxy: {
      '/api': {
        target: process.env.VITE_PROXY_TARGET || 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
})

Объяснение стратегий кеширования

CacheFirst (для медиа и шрифтов)

Request → Cache hit? → Yes → Return cached
                     → No  → Network → Cache + Return
Почему: Фото вещей иммутабельны (URL = хеш содержимого). Нет смысла перезагружать.

StaleWhileRevalidate (для Reference API)

Request → Cache hit? → Yes → Return cached + Revalidate in background
                     → No  → Network → Cache + Return
Почему: Справочники меняются редко, но когда меняются — нужно обновить.

Precache (для статики)

Build time → Generate manifest → Install SW → Cache all assets
Почему: JS/CSS/HTML критичны для работы app. Должны быть доступны offline.

Precache Manifest

При сборке (vite build) генерируется sw.js со списком всех статических файлов:

// Автогенерированный precache manifest
self.__WB_MANIFEST = [
  { url: '/assets/index-abc123.js', revision: null },
  { url: '/assets/index-def456.css', revision: null },
  { url: '/index.html', revision: '...' },
  { url: '/icons/icon-192.png', revision: '...' },
  // ...
]

Bootstrap и регистрация SW

Файл: frontend/src/main.tsx

async function bootstrap() {
  // 1. Гидратация auth state из localStorage
  useAuthStore.getState().hydrate();

  // 2. Запуск sync engine (подписка на network events)
  import('./offline/sync/syncEngine').then(({ syncEngine }) => {
    syncEngine.start();
  });

  // 3. Если авторизован — начать initial sync
  if (useAuthStore.getState().isAuthenticated) {
    import('./offline/services/initialSync').then(({ performInitialSync, registerRefreshSyncListener }) => {
      performInitialSync();
      registerRefreshSyncListener();
    });
  }

  // 4. Рендер React приложения
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <ConfigProvider locale={ruRU} theme={antdTheme}>
        <App />
      </ConfigProvider>
    </React.StrictMode>,
  );
}

bootstrap();

Порядок инициализации: 1. Auth state гидратация (sync) 2. Sync engine start (async, dynamic import) 3. Initial sync (async, только если authenticated) 4. React render (не ждёт sync)