support offline by service worker
Deploy app frontend / app frontend (push) Successful in 1m50s

This commit is contained in:
2024-12-08 13:34:21 +02:00
parent e6f6c9d52d
commit d7b92b2d70
7 changed files with 328 additions and 23 deletions
+43
View File
@@ -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;
}
+124
View File
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#121212" />
<title>Загрузка… — НаСвязи</title>
<link rel="stylesheet" href="/css/offline.css" />
</head>
<body>
<div id="loader">
<div id="offline-message" style="display: none;">
<h1>Нет подключения к интернету</h1>
<p>Проверьте подключение к сети или попробуйте ещё раз.</p>
<button id="retry-button">Попробовать снова</button>
</div>
<div class="spinner" style="display: none;"></div>
</div>
<script>
const offlineMessage = document.getElementById('offline-message');
const spinner = document.querySelector('.spinner');
const retryButton = document.getElementById('retry-button');
function setTitle(message) {
document.title = `${message} — НаСвязи`;
}
function showOfflineMessage() {
offlineMessage.style.display = 'block';
spinner.style.display = 'none';
setTitle('Проблемы с подключением');
}
function showLoadingMessage() {
offlineMessage.style.display = 'none';
spinner.style.display = 'block';
setTitle('Загрузка…');
}
async function retryConnection() {
showLoadingMessage();
try {
const response = await fetch('/', { method: 'HEAD' });
if (response.ok) {
location.reload(); // Перезагружаем страницу при успешном подключении
} else {
throw new Error('Сервер не отвечает');
}
} catch (error) {
console.error(error);
showOfflineMessage();
}
}
function handleOnlineStatus() {
location.reload(); // Перезагрузка при восстановлении сети
}
async function checkConnectionWithServiceWorker() {
if (!navigator.onLine) {
return false;
}
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
return new Promise((resolve) => {
const messageChannel = new MessageChannel();
// Обрабатываем ответ от сервис-воркера
messageChannel.port1.onmessage = (event) => {
resolve(event.data.online);
};
// Посылаем сообщение в сервис-воркер
navigator.serviceWorker.controller.postMessage(
{ type: 'CHECK_ONLINE_STATUS' },
[messageChannel.port2]
);
});
} else {
// Если сервис-воркер недоступен, возвращаем состояние navigator.onLine
return navigator.onLine;
}
}
checkConnectionWithServiceWorker().then((isOnline) => {
if (!isOnline) showOfflineMessage();
})
if ('serviceWorker' in navigator) {
navigator.serviceWorker.onmessage = (event) => {
console.log('onmessage', event.data && event.data.type)
if (event.data && event.data.type === 'MAIN_PAGE_READY') {
location.reload();
}
if (event.data && event.data.type === 'NETWORK_ERROR') {
showOfflineMessage();
}
};
}
retryButton.addEventListener('click', retryConnection);
window.addEventListener('online', handleOnlineStatus);
addEventListener("offline", showOfflineMessage);
showLoadingMessage();
document.addEventListener('DOMContentLoaded', () => {
if (!navigator.onLine) {
return;
}
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({ type: 'UPDATE_STATIC_ASSETS' });
console.log('Message sent to Service Worker: UPDATE_STATIC_ASSETS');
} else {
console.warn('Service Worker is not available or not controlling this page.');
}
});
</script>
</body>
</html>
+142 -12
View File
@@ -1,25 +1,155 @@
// const VER = 14 // const VER = 18
self.addEventListener('message', async (event) => { const CACHE_NAME = 'cache-v1'
event.waitUntil(debug(Object.assign({ answer: true }, event.data))) const TEMP_PAGE_URL = '/offline.html'
}) const STATIC_ASSETS = [TEMP_PAGE_URL, '/css/offline.css']
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
// The promise that skipWaiting() returns can be safely ignored. event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)))
event.waitUntil(self.skipWaiting().then(() => debug({ installed: true })))
// Perform any other actions required for your
// service worker to install, potentially inside
// of event.waitUntil();
}) })
self.addEventListener('activate', (event) => { self.addEventListener('activate', (event) => {
event.waitUntil( event.waitUntil(
// Привязка нового сервис-воркера к текущим клиентам caches.keys().then((cacheNames) =>
clients.claim().then(() => debug(event.source, { activated: true })), 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) => { self.addEventListener('push', async (event) => {
debug({ debug({
event: 'push', event: 'push',
+9 -1
View File
@@ -13,7 +13,15 @@ const debug = (...data: unknown[]) => {
provide('debug', debug) 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(() => { // .then(() => {
// // console.log('Service Worker успешно зарегистрирован') // // console.log('Service Worker успешно зарегистрирован')
// navigator.serviceWorker.addEventListener('message', (event) => { // navigator.serviceWorker.addEventListener('message', (event) => {
+3 -3
View File
@@ -22,12 +22,12 @@
<span> </span> <span> </span>
<span class="text-nowrap"> <span class="text-nowrap">
Будем рады Будем рады
<a <RouterLink
href="/donate" to="/donate"
class="underline underline-offset-2 text-primary-600 dark:text-primary-400" class="underline underline-offset-2 text-primary-600 dark:text-primary-400"
> >
вашей поддержке вашей поддержке
</a> </RouterLink>
</span> </span>
</footer> </footer>
</slot> </slot>
+1 -1
View File
@@ -29,7 +29,7 @@ export async function registerServiceWorker() {
// }, // },
// {}, // {},
// ) // )
//
return registration return registration
// } catch (error) { // } catch (error) {
// console.error('Ошибка регистрации Service Worker:', error) // console.error('Ошибка регистрации Service Worker:', error)
+6 -6
View File
@@ -27,17 +27,17 @@
> >
<p> <p>
Нужен такой же? Нужен такой же?
<a <RouterLink
href="/create" to="/create"
class="underline underline-offset-2 text-primary-600 dark:text-primary-400" class="underline underline-offset-2 text-primary-600 dark:text-primary-400"
>Создайте свой QRкод</a >Создайте свой QRкод</RouterLink
>. >.
</p> </p>
<p class="mt-1"> <p class="mt-1">
Сделайте НаСвязи удобнее. Будем рады вашей&nbsp;<a Сделайте НаСвязи удобнее. Будем рады вашей&nbsp;<RouterLink
href="/donate" to="/donate"
class="underline underline-offset-2 text-primary-600 dark:text-primary-400" class="underline underline-offset-2 text-primary-600 dark:text-primary-400"
>поддержке</a >поддержке</RouterLink
>. >.
</p> </p>
</footer> </footer>