From d7b92b2d70a0321f4105111e6b8f9fe8d27e6022 Mon Sep 17 00:00:00 2001 From: Vasilii Mikhailovskii Date: Sun, 8 Dec 2024 13:34:21 +0200 Subject: [PATCH] support offline by service worker --- apps/frontend/public/css/offline.css | 43 ++++++ apps/frontend/public/offline.html | 124 +++++++++++++++ apps/frontend/public/service-worker.js | 154 +++++++++++++++++-- apps/frontend/src/App.vue | 10 +- apps/frontend/src/layouts/LayoutWithTabs.vue | 6 +- apps/frontend/src/registerServiceWorker.ts | 2 +- apps/frontend/src/views/ReadQRCodeView.vue | 12 +- 7 files changed, 328 insertions(+), 23 deletions(-) create mode 100644 apps/frontend/public/css/offline.css create mode 100644 apps/frontend/public/offline.html diff --git a/apps/frontend/public/css/offline.css b/apps/frontend/public/css/offline.css new file mode 100644 index 0000000..03f5470 --- /dev/null +++ b/apps/frontend/public/css/offline.css @@ -0,0 +1,43 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #121212; + color: #ffffff; + font-family: Arial, sans-serif; +} + +#loader { + text-align: center; +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid rgba(255, 255, 255, 0.3); + border-top-color: #ffffff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 20px auto; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +button { + padding: 10px 20px; + background-color: #007bff; + border: none; + color: white; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} diff --git a/apps/frontend/public/offline.html b/apps/frontend/public/offline.html new file mode 100644 index 0000000..a6d848d --- /dev/null +++ b/apps/frontend/public/offline.html @@ -0,0 +1,124 @@ + + + + + + + Загрузка… — НаСвязи + + + +
+ + +
+ + + + diff --git a/apps/frontend/public/service-worker.js b/apps/frontend/public/service-worker.js index 04338c2..2e76d07 100644 --- a/apps/frontend/public/service-worker.js +++ b/apps/frontend/public/service-worker.js @@ -1,25 +1,155 @@ -// const VER = 14 +// const VER = 18 -self.addEventListener('message', async (event) => { - event.waitUntil(debug(Object.assign({ answer: true }, event.data))) -}) +const CACHE_NAME = 'cache-v1' +const TEMP_PAGE_URL = '/offline.html' +const STATIC_ASSETS = [TEMP_PAGE_URL, '/css/offline.css'] self.addEventListener('install', (event) => { - // The promise that skipWaiting() returns can be safely ignored. - event.waitUntil(self.skipWaiting().then(() => debug({ installed: true }))) - - // Perform any other actions required for your - // service worker to install, potentially inside - // of event.waitUntil(); + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))) }) self.addEventListener('activate', (event) => { event.waitUntil( - // Привязка нового сервис-воркера к текущим клиентам - clients.claim().then(() => debug(event.source, { activated: true })), + caches.keys().then((cacheNames) => + Promise.all( + cacheNames.map((cache) => { + if (cache !== CACHE_NAME) { + return caches.delete(cache) + } + }), + ), + ), ) }) +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url) + + // Обрабатываем только HTTP/HTTPS запросы + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return + } + + if (event.request.mode === 'navigate') { + // Для навигационных запросов сначала отдаём `/offline.html`, пока основная страница загружается + event.respondWith(handleNavigationRequest(event.request)) + } else if (STATIC_ASSETS.includes(url.pathname)) { + // Кэшируем только offline.html и его CSS + event.respondWith(caches.match(event.request)) + } +}) + +const TEMP_CACHE = new Map() // Используем карту для временного хранения + +async function handleNavigationRequest(request) { + if (TEMP_CACHE.has(request.url) && navigator.onLine) { + return TEMP_CACHE.get(request.url).clone() + } + + const cache = await caches.open(CACHE_NAME) + + // Отдаём временную страницу из кеша + const tempResponse = await cache.match(TEMP_PAGE_URL) + + // Попытка загрузить основную страницу в фоновом режиме + fetch(request) + .then(async (networkResponse) => { + if (networkResponse && networkResponse.ok) { + // Сохраняем клон в TEMP_CACHE + TEMP_CACHE.set(request.url, networkResponse) + + // Сообщаем клиенту, что основная страница готова + const allClients = await clients.matchAll({ type: 'window' }) + allClients.forEach((client) => + client.postMessage({ + type: 'MAIN_PAGE_READY', + }), + ) + + // Удаляем из TEMP_CACHE через минимальное время + setTimeout(() => { + TEMP_CACHE.delete(request.url) + }, 5000) + } else { + throw new Error('Failed to fetch main page') + } + }) + .catch(async (error) => { + console.error('Error loading main page:', error) + + // Сообщаем клиенту об ошибке сети + const allClients = await clients.matchAll({ type: 'window' }) + allClients.forEach((client) => + client.postMessage({ + type: 'NETWORK_ERROR', + }), + ) + }) + + // Возвращаем временную страницу + return tempResponse +} + +async function updateStaticAssetsCache() { + const cache = await caches.open(CACHE_NAME) + + // Обновляем ресурсы + try { + await Promise.all( + STATIC_ASSETS.map(async (asset) => { + const response = await fetch(asset, { cache: 'no-store' }) + if (response.ok) { + await cache.put(asset, response.clone()) + } else { + console.error(`Failed to fetch ${asset}:`, response.status) + } + }), + ) + console.log('STATIC_ASSETS обновлены.') + } catch (error) { + console.error('Ошибка обновления STATIC_ASSETS:', error) + } +} + +self.addEventListener('message', async (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } else if (event.data && event.data.type === 'CHECK_ONLINE_STATUS') { + const isOnline = navigator.onLine // Проверяем состояние сети + event.ports[0].postMessage({ online: isOnline }) // Отправляем результат обратно + } else if (event.data && event.data.type === 'UPDATE_STATIC_ASSETS') { + event.waitUntil(updateStaticAssetsCache()) + } else { + event.waitUntil(debug(Object.assign({ answer: true }, event.data))) + } +}) + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(STATIC_ASSETS) + }), + ) + + self.skipWaiting().then(() => debug({ installed: true })) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => + Promise.all( + cacheNames.map((cache) => { + if (cache !== CACHE_NAME) { + return caches.delete(cache) + } + }), + ), + ), + ) + + clients.claim().then(() => debug(event.source, { activated: true })) +}) + self.addEventListener('push', async (event) => { debug({ event: 'push', diff --git a/apps/frontend/src/App.vue b/apps/frontend/src/App.vue index 58f185c..362fe88 100644 --- a/apps/frontend/src/App.vue +++ b/apps/frontend/src/App.vue @@ -13,7 +13,15 @@ const debug = (...data: unknown[]) => { provide('debug', debug) -registerServiceWorker() +const serviceWorkerPromise = registerServiceWorker() + +onMounted(async () => { + await serviceWorkerPromise + navigator.serviceWorker?.controller?.postMessage({ + type: 'UPDATE_STATIC_ASSETS', + }) + console.log('Message sent to Service Worker: UPDATE_STATIC_ASSETS') +}) // .then(() => { // // console.log('Service Worker успешно зарегистрирован') // navigator.serviceWorker.addEventListener('message', (event) => { diff --git a/apps/frontend/src/layouts/LayoutWithTabs.vue b/apps/frontend/src/layouts/LayoutWithTabs.vue index eed41e5..88b2ad9 100644 --- a/apps/frontend/src/layouts/LayoutWithTabs.vue +++ b/apps/frontend/src/layouts/LayoutWithTabs.vue @@ -22,12 +22,12 @@ Будем рады - вашей поддержке - + diff --git a/apps/frontend/src/registerServiceWorker.ts b/apps/frontend/src/registerServiceWorker.ts index 5e2eb95..0c98bba 100644 --- a/apps/frontend/src/registerServiceWorker.ts +++ b/apps/frontend/src/registerServiceWorker.ts @@ -29,7 +29,7 @@ export async function registerServiceWorker() { // }, // {}, // ) - // + return registration // } catch (error) { // console.error('Ошибка регистрации Service Worker:', error) diff --git a/apps/frontend/src/views/ReadQRCodeView.vue b/apps/frontend/src/views/ReadQRCodeView.vue index b2809d6..1f6c363 100644 --- a/apps/frontend/src/views/ReadQRCodeView.vue +++ b/apps/frontend/src/views/ReadQRCodeView.vue @@ -27,17 +27,17 @@ >

Нужен такой же? - Создайте свой QR‑кодСоздайте свой QR‑код.

- Сделайте НаСвязи удобнее. Будем рады вашей поддержкеподдержке.