Обновлена конфигурация Vite для PWA с улучшенной обработкой навигации и кэширования HTML-документов. Добавлены новые правила кэширования для страниц и HTML-файлов, а также изменен тип регистрации сервисного работника на автоматическое обновление. Обновлен HTML-шаблон с улучшениями в загрузке и отображении темы.

This commit is contained in:
Fovway 2025-11-06 23:29:34 +07:00
parent e6acd8c5df
commit d023e20862
5 changed files with 363 additions and 195 deletions

131
OFFLINE_FIX.md Normal file
View File

@ -0,0 +1,131 @@
# Исправление проблемы с оффлайн режимом
## Проблема
При обновлении страницы в оффлайн режиме попеременно показывается сообщение "нет подключения к интернету" и нормальное отображение приложения.
## Причина
Service Worker использовал стратегию, которая пыталась обратиться к сети перед использованием кэша, что приводило к показу страницы ошибки браузера при отсутствии подключения.
## Решение
Внесены изменения в `vite.config.ts` для обеспечения стабильной работы в оффлайн режиме:
### 1. Стратегия CacheFirst для навигационных запросов
Теперь при обновлении страницы Service Worker **сразу** загружает кэшированную версию без попыток обращения к сети:
```typescript
{
urlPattern: ({ request }) => request.mode === 'navigate',
handler: "CacheFirst",
options: {
cacheName: "pages-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
}
```
### 2. Дополнительное кэширование HTML файлов
Добавлено специальное правило для всех HTML файлов:
```typescript
{
urlPattern: /\.html$/,
handler: "CacheFirst",
options: {
cacheName: "html-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 7 * 24 * 60 * 60,
},
cacheableResponse: {
statuses: [0, 200],
},
},
}
```
### 3. Автоматическое обновление Service Worker
Изменен тип регистрации с `"prompt"` на `"autoUpdate"` для обеспечения автоматического обновления SW без запроса пользователя:
```typescript
registerType: "autoUpdate"
```
### 4. Отключение Navigation Preload
Добавлена настройка `navigationPreload: false` для предотвращения попыток загрузки из сети при навигации.
## Как применить изменения
### 1. Пересборка проекта
```bash
npm run build
```
### 2. Копирование файлов на сервер
После успешной сборки скопируйте содержимое папки `dist/` в `backend/public/`:
```bash
# Windows PowerShell
Remove-Item -Recurse -Force backend/public/*
Copy-Item -Recurse dist/* backend/public/
# Linux/Mac
rm -rf backend/public/*
cp -r dist/* backend/public/
```
### 3. Запуск сервера
Запустите сервер (или попросите это сделать):
```bash
cd backend
npm start
```
### 4. Проверка работы
1. Откройте приложение в браузере
2. Дождитесь полной загрузки приложения
3. Откройте DevTools → Application → Service Workers
4. Убедитесь, что новый Service Worker активирован
5. Переключите браузер в оффлайн режим (DevTools → Network → Offline)
6. Обновите страницу несколько раз - приложение должно загружаться из кэша каждый раз
### 5. Очистка старого кэша (если необходимо)
Если проблема все еще возникает, попробуйте:
1. DevTools → Application → Clear storage → Clear site data
2. Закройте и откройте браузер заново
3. Перейдите на сайт снова - новый SW установится и закэширует все ресурсы
4. Проверьте работу в оффлайн режиме
## Технические детали
### Стратегии кэширования
**CacheFirst (Cache First, Network Fallback)**
- Сначала проверяется кэш
- Если ресурс найден в кэше, он возвращается немедленно
- Если ресурса нет в кэше, делается запрос к сети
- Идеально для статических ресурсов и оффлайн работы
**NetworkFirst** (используется для API)
- Сначала пытается загрузить из сети
- Если сеть недоступна или таймаут, возвращается кэшированная версия
- Идеально для данных, которые должны быть актуальными
### Настройки Service Worker
- `skipWaiting: true` - новый SW активируется немедленно
- `clientsClaim: true` - новый SW берет контроль над всеми открытыми страницами
- `cleanupOutdatedCaches: true` - автоматическая очистка устаревших кэшей
## Ожидаемый результат
После применения изменений приложение будет:
- ✅ Всегда загружаться из кэша в оффлайн режиме
- ✅ Не показывать страницу "нет подключения к интернету"
- ✅ Работать стабильно при любом количестве обновлений страницы
- ✅ Автоматически обновлять Service Worker без запроса пользователя

View File

@ -1,69 +1,69 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>NoteJS - Система заметок</title>
<!-- Предотвращение мерцания темы -->
<script>
(function () {
try {
// Получаем сохраненную тему
const savedTheme = localStorage.getItem("theme");
// Получаем системные предпочтения
const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
// Определяем тему: сохраненная или системная
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
// Функция для конвертации hex в RGB
function hexToRgb(hex) {
const cleanHex = hex.replace("#", "");
const r = parseInt(cleanHex.substring(0, 2), 16);
const g = parseInt(cleanHex.substring(2, 4), 16);
const b = parseInt(cleanHex.substring(4, 6), 16);
return `${r}, ${g}, ${b}`;
}
// Получаем и устанавливаем accentColor
const savedAccentColor = localStorage.getItem("accentColor");
const accentColor = savedAccentColor || "#007bff";
// Устанавливаем тему и переменные до загрузки CSS
if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "light");
}
// Устанавливаем CSS переменные для accent цвета
document.documentElement.style.setProperty("--accent-color", accentColor);
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
// Устанавливаем цвет для meta theme-color
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) {
themeColorMeta.setAttribute(
"content",
theme === "dark" ? "#1a1a1a" : accentColor
);
}
} catch (e) {
// В случае ошибки устанавливаем светлую тему по умолчанию
document.documentElement.setAttribute("data-theme", "light");
document.documentElement.style.setProperty("--accent-color", "#007bff");
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
}
})();
</script>
<!-- Критические стили темы для предотвращения flash эффекта -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>NoteJS - Система заметок</title>
<!-- Предотвращение мерцания темы -->
<script>
(function () {
try {
// Получаем сохраненную тему
const savedTheme = localStorage.getItem("theme");
// Получаем системные предпочтения
const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
// Определяем тему: сохраненная или системная
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
// Функция для конвертации hex в RGB
function hexToRgb(hex) {
const cleanHex = hex.replace("#", "");
const r = parseInt(cleanHex.substring(0, 2), 16);
const g = parseInt(cleanHex.substring(2, 4), 16);
const b = parseInt(cleanHex.substring(4, 6), 16);
return `${r}, ${g}, ${b}`;
}
// Получаем и устанавливаем accentColor
const savedAccentColor = localStorage.getItem("accentColor");
const accentColor = savedAccentColor || "#007bff";
// Устанавливаем тему и переменные до загрузки CSS
if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "light");
}
// Устанавливаем CSS переменные для accent цвета
document.documentElement.style.setProperty("--accent-color", accentColor);
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
// Устанавливаем цвет для meta theme-color
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) {
themeColorMeta.setAttribute(
"content",
theme === "dark" ? "#1a1a1a" : accentColor
);
}
} catch (e) {
// В случае ошибки устанавливаем светлую тему по умолчанию
document.documentElement.setAttribute("data-theme", "light");
document.documentElement.style.setProperty("--accent-color", "#007bff");
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
}
})();
</script>
<!-- Критические стили темы для предотвращения flash эффекта -->
<style>
:root {
--accent-color: #007bff;
@ -89,94 +89,94 @@
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
</style>
<!-- PWA Meta Tags -->
<meta
name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/>
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link
rel="apple-touch-icon"
sizes="114x114"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/icons/icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
</style>
<!-- PWA Meta Tags -->
<meta
name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/>
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link
rel="apple-touch-icon"
sizes="114x114"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/icons/icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-B61qRIc-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DK8OUj6L.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body>
<div id="root">
<!-- Индикатор загрузки до монтирования React -->
<div id="initial-loading" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body>
<div id="root">
<!-- Индикатор загрузки до монтирования React -->
<div id="initial-loading" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
">
<style>
#initial-loading-spinner {
width: 50px;
@ -193,43 +193,43 @@
@keyframes initial-loading-spin {
to { transform: rotate(360deg); }
}
</style>
<div id="initial-loading-spinner"></div>
</div>
</div>
<script>
// Скрываем индикатор загрузки сразу после загрузки DOM
// React удалит этот элемент при первом рендере через createRoot
(function() {
// Используем MutationObserver для отслеживания изменений в #root
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// Если React начал добавлять элементы в #root, удаляем индикатор
if (mutation.addedNodes.length > 0) {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}
});
});
// Начинаем наблюдение за изменениями в #root
const root = document.getElementById('root');
if (root) {
observer.observe(root, { childList: true, subtree: true });
// Фолбэк: если через 2 секунды элемент все еще есть, удаляем вручную
setTimeout(function() {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}, 2000);
}
})();
</script>
</body>
</html>
</style>
<div id="initial-loading-spinner"></div>
</div>
</div>
<script>
// Скрываем индикатор загрузки сразу после загрузки DOM
// React удалит этот элемент при первом рендере через createRoot
(function() {
// Используем MutationObserver для отслеживания изменений в #root
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// Если React начал добавлять элементы в #root, удаляем индикатор
if (mutation.addedNodes.length > 0) {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}
});
});
// Начинаем наблюдение за изменениями в #root
const root = document.getElementById('root');
if (root) {
observer.observe(root, { childList: true, subtree: true });
// Фолбэк: если через 2 секунды элемент все еще есть, удаляем вручную
setTimeout(function() {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}, 2000);
}
})();
</script>
</body>
</html>

View File

@ -1 +1 @@
if(!self.define){let e,i={};const n=(n,c)=>(n=new URL(n+".js",c).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let a={};const d=e=>n(e,s),r={module:{uri:s},exports:a,require:d};i[s]=Promise.all(c.map(e=>r[e]||d(e))).then(e=>(o(...e),a))}}define(["./workbox-e20531c6"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-B61qRIc-.js",revision:"96888e49126c254a0b6fb7a9428bddb6"},{url:"assets/index-DK8OUj6L.css",revision:"b1e2c4e8724be2f2bcee585338910e99"},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"45ec10836831308e81415ddb8cc82efd"},{url:"logo.svg",revision:"5962d0d24d9cd26cd8aaff9cb6f54a5a"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didnt register its module`);return e}));self.define=(c,s)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(n[o])return;let a={};const r=e=>i(e,o),d={module:{uri:o},exports:a,require:r};n[o]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(s(...e),a))}}define(["./workbox-40c80ae4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-B61qRIc-.js",revision:"96888e49126c254a0b6fb7a9428bddb6"},{url:"assets/index-DK8OUj6L.css",revision:"b1e2c4e8724be2f2bcee585338910e99"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"ac16c70d736d7f92715a884f73979e8e"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(({request:e})=>"navigate"===e.mode,new e.CacheFirst({cacheName:"pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\.html$/,new e.CacheFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});

File diff suppressed because one or more lines are too long

View File

@ -91,9 +91,11 @@ export default defineConfig({
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
// Игнорируем параметры URL при кешировании
ignoreURLParametersMatching: [/^utm_/, /^fbclid$/],
// Обработка ошибок при precaching - не падаем на 404
// Обработка навигации - всегда используем кэшированную версию
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api/, /^\/uploads/],
// Стратегия для навигационных запросов - сначала кэш
navigationPreload: false,
// Обработка ошибок при загрузке файлов для precaching
dontCacheBustURLsMatching: /\.\w{8}\./,
// Фильтруем манифест, чтобы исключить несуществующие файлы и дубликаты
@ -113,6 +115,38 @@ export default defineConfig({
},
],
runtimeCaching: [
// HTML документы - всегда сначала кэш для оффлайн работы
// Это гарантирует, что при обновлении страницы в оффлайн режиме
// всегда будет загружаться кэшированная версия БЕЗ попыток обращения к сети
{
urlPattern: ({ request }) => request.mode === 'navigate',
handler: "CacheFirst",
options: {
cacheName: "pages-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
// Дополнительное правило для HTML файлов
{
urlPattern: /\.html$/,
handler: "CacheFirst",
options: {
cacheName: "html-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/api\./,
handler: "NetworkFirst",
@ -165,7 +199,9 @@ export default defineConfig({
// Обработка ошибок при precaching - игнорируем 404 ошибки
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
},
registerType: "prompt",
// Автоматическое обновление SW без запроса пользователя
// для обеспечения стабильной работы в оффлайн режиме
registerType: "autoUpdate",
devOptions: {
enabled: true,
type: "module",