Offline-first застосунок із Service Workers
Service Worker — це фоновий JavaScript-файл, який перехоплює мережеві запити, даючи змогу віддавати вашу симуляцію з кешу, коли немає інтернет-з'єднання. Цей урок охоплює життєвий цикл SW, стратегії cache-first та stale-while-revalidate, Web App Manifest для встановлюваності на головний екран і практичне порівняння ванільного SW з Workbox.
- Базовий JavaScript (async/await, проміси, fetch)
- Розуміння кешування в браузері на концептуальному рівні
-
Локальний сервер (розширення Live Server,
npx serve) — SW потребує HTTPS або localhost
Життєвий цикл Service Worker
Розуміння життєвого циклу усуває проблему №1 із SW: чому мої зміни не відображаються?
-
Register (реєстрація) — головна сторінка викликає
navigator.serviceWorker.register('/sw.js'). Браузер завантажує та парсить файл SW. -
Install (встановлення) — SW спрацьовує подією
install. Використовуйте її для попереднього кешування ресурсів. SW «встановлений», але ще не «активний». -
Wait (очікування) — якщо старий SW досі керує
сторінками, новий SW чекає. Викличте
self.skipWaiting(), щоб пропустити це. -
Activate (активація) — SW перебирає контроль.
Використовуйте її для очищення старих кешів. Викличте
clients.claim(), щоб негайно перебрати контроль над наявними сторінками. - Fetch (запит) — SW перехоплює кожен мережевий запит від керованих сторінок. Ви вирішуєте: віддавати з кешу, завантажувати з мережі чи поєднувати обидва підходи.
Щоб примусово оновити SW під час розробки: DevTools → Application →
Service Workers → «Update on reload» і «Bypass for network». Або
змініть константу CACHE_VERSION, щоб запустити
install/activate.
Реєстрація SW зі сторінки
// У вашому головному HTML- або JS-файлі
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', {
scope: '/', // SW керує всіма сторінками під /
});
console.log('SW registered, scope:', reg.scope);
// Опціонально: слухаємо оновлення
reg.addEventListener('updatefound', () => {
const newSW = reg.installing;
newSW.addEventListener('statechange', () => {
if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
// Доступний новий вміст — показуємо користувачу запит "Оновити"
console.log('New version available!');
}
});
});
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
Подія install — попереднє кешування ресурсів
Під час встановлення завантажте та збережіть кожен критичний ресурс. SW не активується, доки всі ресурси не закешуються успішно:
// sw.js
const CACHE_VERSION = 'v2'; // збільшіть це, щоб запустити оновлення
const CACHE_NAME = `sim-cache-${CACHE_VERSION}`;
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/offline.html',
'/shared/theme.css',
'/shared/components.css',
'/shared/components.js',
'/manifest.json',
// Додайте HTML-файли своїх симуляцій:
'/fluid/index.html',
'/pathfinding/index.html',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Precaching assets...');
return cache.addAll(PRECACHE_ASSETS);
})
);
self.skipWaiting(); // Не чекаємо — перебираємо контроль негайно
});
Подія fetch — стратегія cache-first
Cache-first: спершу пробуємо кеш. Якщо знайдено — повертаємо одразу. Якщо ні — завантажуємо з мережі та кешуємо відповідь на наступний раз. Найкраще для статичних ресурсів:
self.addEventListener('fetch', event => {
// Обробляємо лише GET-запити
if (event.request.method !== 'GET') return;
event.respondWith(cacheFirst(event.request));
});
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached; // ← миттєво віддаємо з кешу
try {
const response = await fetch(request);
if (response.ok) {
// Кешуємо клон (відповідь можна спожити лише раз)
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
// Повністю офлайн і немає в кеші
return caches.match('/offline.html');
}
}
Cache-First
Віддаємо з кешу. Швидко, працює офлайн. Застаріле, доки SW не оновиться. Використовуйте для: JS, CSS, шрифтів.
Network-First
Пробуємо мережу. У разі невдачі повертаємось до кешу. Свіже онлайн, придатне офлайн. Використовуйте для: API, HTML.
Stale-While-Revalidate
Віддаємо кеш одразу, потім оновлюємо кеш у фоні. Завжди швидко, згодом свіже. Використовуйте для: аватарів, некритичних ресурсів.
Network-Only
Завжди завантажуємо. Ніколи не кешуємо. Використовуйте для: аналітики, платежів, усього, що має бути свіжим.
Activate — очищення старих кешів
Коли активується новий SW, видаляйте кеші попередніх версій, щоб звільнити сховище:
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([
// Видаляємо всі кеші, крім поточної версії
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => {
console.log('Deleting old cache:', key);
return caches.delete(key);
})
)
),
// Негайно перебираємо контроль над усіма відкритими вкладками
self.clients.claim(),
])
);
});
Стратегія stale-while-revalidate
Миттєво віддаємо кешовану версію, потім оновлюємо кеш у фоні. Користувач отримує швидку відповідь, а наступний візит — свіжий вміст:
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
// Повторно валідуємо у фоні незалежно від влучання в кеш
const fetchPromise = fetch(request).then(response => {
if (response.ok) cache.put(request, response.clone());
return response;
}).catch(() => null); // ігноруємо невдачу, якщо офлайн
// Повертаємо кеш одразу або чекаємо на мережу, якщо кешу немає
return cached ?? fetchPromise;
}
// Застосовуємо різні стратегії для різних типів запитів:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// CSS/JS: cache-first (версіоновані, рідко змінюються)
if (url.pathname.match(/\.(css|js)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// HTML-сторінки: stale-while-revalidate
if (event.request.headers.get('accept')?.includes('text/html')) {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Усе інше: network-first
event.respondWith(networkFirst(event.request));
});
Web App Manifest — робимо застосунок встановлюваним
За наявності коректного маніфесту та зареєстрованого SW Chrome/Edge показують запит «Встановити». Застосунок можна додати на головний екран, і він відкривається на весь екран без елементів інтерфейсу браузера:
// manifest.json
{
"name": "3D Simulations",
"short_name": "3DSims",
"description": "Interactive physics and algorithm simulations",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a10",
"theme_color": "#22c55e",
"orientation": "any",
"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" }
],
"screenshots": [
{ "src": "/screenshots/fluid.jpg", "sizes": "1280x720", "type": "image/jpeg",
"form_factor": "wide" }
]
}
<!-- У <head> кожної сторінки: -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#22c55e" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
Чекліст PWA: SW має працювати на HTTPS (або
localhost), маніфест повинен мати коректні іконки, а
start_url має бути в межах області дії SW. Для повного
аудиту перевірте Lighthouse (DevTools → Lighthouse → Progressive Web
App).