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 (для медиа и шрифтов)¶
Почему: Фото вещей иммутабельны (URL = хеш содержимого). Нет смысла перезагружать.StaleWhileRevalidate (для Reference API)¶
Request → Cache hit? → Yes → Return cached + Revalidate in background
→ No → Network → Cache + Return
Precache (для статики)¶
Почему: 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)