diff --git a/public/index.html b/public/index.html index 83500e5..c5a74f3 100644 --- a/public/index.html +++ b/public/index.html @@ -4,13 +4,19 @@ Вход в систему заметок - + - + - + @@ -18,28 +24,58 @@ - + - - + + - - - - - + + + + + - + - + - + @@ -86,23 +122,29 @@

Создатель: Fovway

- + diff --git a/public/mobile-install-test.html b/public/mobile-install-test.html new file mode 100644 index 0000000..d0b6361 --- /dev/null +++ b/public/mobile-install-test.html @@ -0,0 +1,230 @@ + + + + + + Тест установки PWA на мобильном + + + + + +
+

📱 Тест установки PWA на мобильном

+ +
+

Инструкции:

+

1. Откройте эту страницу на мобильном устройстве

+

2. Должен появиться нативный баннер установки браузера

+

3. Если баннер не появился, используйте кнопки ниже

+
+ +
+ +
+ + + +
+ +
+

Отладочная информация:

+
+
+
+ + + + diff --git a/public/pwa.js b/public/pwa.js index a19a5d3..ad72825 100644 --- a/public/pwa.js +++ b/public/pwa.js @@ -6,7 +6,7 @@ class PWAManager { } init() { - console.log('PWA Manager инициализирован'); + console.log("PWA Manager инициализирован"); this.registerServiceWorker(); this.setupInstallPrompt(); this.setupAppInstalled(); @@ -16,45 +16,60 @@ class PWAManager { // Проверка требований PWA checkPWARequirements() { - console.log('Проверка требований PWA:'); - console.log('- Service Worker:', 'serviceWorker' in navigator); - console.log('- HTTPS:', location.protocol === 'https:' || location.hostname === 'localhost'); - console.log('- Manifest:', document.querySelector('link[rel="manifest"]') !== null); - console.log('- Icons:', document.querySelector('link[rel="icon"]') !== null); + console.log("Проверка требований PWA:"); + console.log("- Service Worker:", "serviceWorker" in navigator); + console.log( + "- HTTPS:", + location.protocol === "https:" || location.hostname === "localhost" + ); + console.log( + "- Manifest:", + document.querySelector('link[rel="manifest"]') !== null + ); + console.log( + "- Icons:", + document.querySelector('link[rel="icon"]') !== null + ); } // Регистрация Service Worker registerServiceWorker() { - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js') + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("/sw.js") .then((registration) => { - console.log('SW зарегистрирован успешно:', registration.scope); - + console.log("SW зарегистрирован успешно:", registration.scope); + // Проверяем обновления - registration.addEventListener('updatefound', () => { + registration.addEventListener("updatefound", () => { const newWorker = registration.installing; - newWorker.addEventListener('statechange', () => { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + newWorker.addEventListener("statechange", () => { + if ( + newWorker.state === "installed" && + navigator.serviceWorker.controller + ) { this.showUpdateNotification(); } }); }); }) .catch((error) => { - console.log('Ошибка регистрации SW:', error); + console.log("Ошибка регистрации SW:", error); }); }); } else { - console.log('Service Worker не поддерживается'); + console.log("Service Worker не поддерживается"); } } // Показ уведомления об обновлении showUpdateNotification() { - if (confirm('Доступна новая версия приложения. Обновить?')) { + if (confirm("Доступна новая версия приложения. Обновить?")) { if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' }); + navigator.serviceWorker.controller.postMessage({ + type: "SKIP_WAITING", + }); } window.location.reload(); } @@ -62,11 +77,23 @@ class PWAManager { // Настройка промпта установки setupInstallPrompt() { - window.addEventListener('beforeinstallprompt', (e) => { - console.log('beforeinstallprompt событие получено'); + window.addEventListener("beforeinstallprompt", (e) => { + console.log("beforeinstallprompt событие получено"); + + // На мобильных устройствах позволяем браузеру показать нативный баннер + if (this.isMobileDevice()) { + console.log( + "Мобильное устройство - разрешаем нативный баннер установки" + ); + // Не вызываем preventDefault() для мобильных устройств + this.deferredPrompt = e; + return; + } + + // На десктопе показываем кастомную кнопку e.preventDefault(); this.deferredPrompt = e; - + // Показываем кнопку установки с задержкой для лучшего UX setTimeout(() => { this.showInstallButton(); @@ -76,23 +103,25 @@ class PWAManager { // Показ кнопки установки showInstallButton() { - console.log('Попытка показать кнопку установки'); - + console.log("Попытка показать кнопку установки"); + // Проверяем, не установлено ли уже приложение if (this.isPWAInstalled()) { - console.log('Приложение уже установлено'); + console.log("Приложение уже установлено"); return; } - // Показываем кнопку только на мобильных устройствах - if (!this.isMobileDevice()) { - console.log('Кнопка установки скрыта для ПК версии'); + // Показываем кнопку только на десктопе (на мобильных используем нативный баннер) + if (this.isMobileDevice()) { + console.log( + "Мобильное устройство - используем нативный баннер установки" + ); return; } // Проверяем, поддерживает ли браузер установку PWA - if (!this.deferredPrompt && !this.isMobileSafari()) { - console.log('Установка PWA не поддерживается в этом браузере'); + if (!this.deferredPrompt) { + console.log("Установка PWA не поддерживается в этом браузере"); return; } @@ -103,36 +132,44 @@ class PWAManager { // Проверка на мобильное устройство isMobileDevice() { const ua = navigator.userAgent; - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua) || - (navigator.maxTouchPoints && navigator.maxTouchPoints > 2) || - window.matchMedia('(max-width: 768px)').matches || - /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua); + return ( + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + ua + ) || + (navigator.maxTouchPoints && navigator.maxTouchPoints > 2) || + window.matchMedia("(max-width: 768px)").matches || + /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua) + ); } // Проверка на мобильный Safari isMobileSafari() { const ua = navigator.userAgent; - return /iPad|iPhone|iPod/.test(ua) && /Safari/.test(ua) && !/CriOS|FxiOS|OPiOS|mercury/.test(ua); + return ( + /iPad|iPhone|iPod/.test(ua) && + /Safari/.test(ua) && + !/CriOS|FxiOS|OPiOS|mercury/.test(ua) + ); } // Создание кнопки установки createInstallButton() { - const installButton = document.createElement('button'); - + const installButton = document.createElement("button"); + // Разный текст для разных браузеров if (this.isMobileSafari()) { - installButton.textContent = '📱 Добавить на главный экран'; + installButton.textContent = "📱 Добавить на главный экран"; } else { - installButton.textContent = '📱 Установить приложение'; + installButton.textContent = "📱 Установить приложение"; } - - installButton.className = 'btnSave'; - installButton.style.marginTop = '10px'; - installButton.style.width = '100%'; - installButton.style.fontSize = '14px'; - installButton.id = 'pwa-install-button'; - installButton.addEventListener('click', () => { + installButton.className = "btnSave"; + installButton.style.marginTop = "10px"; + installButton.style.width = "100%"; + installButton.style.fontSize = "14px"; + installButton.id = "pwa-install-button"; + + installButton.addEventListener("click", () => { this.installApp(); }); @@ -142,57 +179,77 @@ class PWAManager { // Добавление кнопки на страницу addInstallButtonToPage(installButton) { // Удаляем существующую кнопку, если есть - const existingButton = document.getElementById('pwa-install-button'); + const existingButton = document.getElementById("pwa-install-button"); if (existingButton) { existingButton.remove(); } // Ищем подходящее место для кнопки - const authLink = document.querySelector('.auth-link'); - const footer = document.querySelector('.footer'); - const container = document.querySelector('.container'); + const authLink = document.querySelector(".auth-link"); + const footer = document.querySelector(".footer"); + const container = document.querySelector(".container"); if (authLink) { authLink.appendChild(installButton); - console.log('Кнопка установки добавлена в auth-link'); + console.log("Кнопка установки добавлена в auth-link"); } else if (footer) { footer.insertBefore(installButton, footer.firstChild); - console.log('Кнопка установки добавлена в footer'); + console.log("Кнопка установки добавлена в footer"); } else if (container) { container.appendChild(installButton); - console.log('Кнопка установки добавлена в container'); + console.log("Кнопка установки добавлена в container"); } else { document.body.appendChild(installButton); - console.log('Кнопка установки добавлена в body'); + console.log("Кнопка установки добавлена в body"); } } // Установка приложения installApp() { - console.log('Попытка установки приложения'); - + console.log("Попытка установки приложения"); + if (this.isMobileSafari()) { // Для iOS Safari показываем инструкции this.showSafariInstructions(); return; } - + if (this.deferredPrompt) { - this.deferredPrompt.prompt(); - this.deferredPrompt.userChoice.then((choiceResult) => { - console.log('Результат установки:', choiceResult.outcome); - if (choiceResult.outcome === 'accepted') { - console.log('Пользователь установил приложение'); - this.showNotification('Приложение успешно установлено!', 'success'); - } else { - console.log('Пользователь отклонил установку'); - this.showNotification('Установка отменена', 'warning'); - } - this.deferredPrompt = null; - this.removeInstallButton(); - }); + // На мобильных устройствах вызываем prompt() для показа нативного баннера + if (this.isMobileDevice()) { + console.log( + "Показываем нативный баннер установки на мобильном устройстве" + ); + this.deferredPrompt.prompt(); + this.deferredPrompt.userChoice.then((choiceResult) => { + console.log("Результат установки:", choiceResult.outcome); + if (choiceResult.outcome === "accepted") { + console.log("Пользователь установил приложение"); + this.showNotification("Приложение успешно установлено!", "success"); + } else { + console.log("Пользователь отклонил установку"); + this.showNotification("Установка отменена", "warning"); + } + this.deferredPrompt = null; + }); + } else { + // На десктопе используем обычную логику + this.deferredPrompt.prompt(); + this.deferredPrompt.userChoice.then((choiceResult) => { + console.log("Результат установки:", choiceResult.outcome); + if (choiceResult.outcome === "accepted") { + console.log("Пользователь установил приложение"); + this.showNotification("Приложение успешно установлено!", "success"); + } else { + console.log("Пользователь отклонил установку"); + this.showNotification("Установка отменена", "warning"); + } + this.deferredPrompt = null; + this.removeInstallButton(); + }); + } } else { - console.log('deferredPrompt не доступен'); + console.log("deferredPrompt не доступен"); this.showManualInstallInstructions(); } } @@ -206,15 +263,16 @@ class PWAManager { // Проверяем все требования PWA const requirements = { hasManifest: document.querySelector('link[rel="manifest"]') !== null, - hasServiceWorker: 'serviceWorker' in navigator, - isSecure: location.protocol === 'https:' || location.hostname === 'localhost', - hasIcons: document.querySelector('link[rel="icon"]') !== null + hasServiceWorker: "serviceWorker" in navigator, + isSecure: + location.protocol === "https:" || location.hostname === "localhost", + hasIcons: document.querySelector('link[rel="icon"]') !== null, }; - const allRequirementsMet = Object.values(requirements).every(req => req); - + const allRequirementsMet = Object.values(requirements).every((req) => req); + if (!allRequirementsMet) { - console.log('Не все требования PWA выполнены:', requirements); + console.log("Не все требования PWA выполнены:", requirements); return false; } @@ -258,9 +316,9 @@ class PWAManager { showMobileInstallInstructions() { const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); - - let instructions = ''; - + + let instructions = ""; + if (isAndroid) { instructions = ` Для установки приложения на Android: @@ -285,33 +343,35 @@ class PWAManager { 3. Следуйте инструкциям браузера `; } - + alert(instructions); } // Удаление кнопки установки removeInstallButton() { - const installButton = document.getElementById('pwa-install-button'); + const installButton = document.getElementById("pwa-install-button"); if (installButton) { installButton.remove(); - console.log('Кнопка установки удалена'); + console.log("Кнопка установки удалена"); } } // Обработка успешной установки setupAppInstalled() { - window.addEventListener('appinstalled', () => { - console.log('PWA установлено успешно'); + window.addEventListener("appinstalled", () => { + console.log("PWA установлено успешно"); this.removeInstallButton(); }); } // Проверка статуса PWA isPWAInstalled() { - return window.matchMedia('(display-mode: standalone)').matches || - window.navigator.standalone === true || - document.referrer.includes('android-app://') || - window.matchMedia('(display-mode: fullscreen)').matches; + return ( + window.matchMedia("(display-mode: standalone)").matches || + window.navigator.standalone === true || + document.referrer.includes("android-app://") || + window.matchMedia("(display-mode: fullscreen)").matches + ); } // Получение информации о PWA @@ -319,59 +379,61 @@ class PWAManager { return { isInstalled: this.isPWAInstalled(), isOnline: navigator.onLine, - hasServiceWorker: 'serviceWorker' in navigator, + hasServiceWorker: "serviceWorker" in navigator, userAgent: navigator.userAgent, hasDeferredPrompt: this.deferredPrompt !== null, isMobileDevice: this.isMobileDevice(), isMobileSafari: this.isMobileSafari(), platform: navigator.platform, language: navigator.language, - displayMode: window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser' + displayMode: window.matchMedia("(display-mode: standalone)").matches + ? "standalone" + : "browser", }; } // Принудительное обновление кэша async forceUpdateCache() { - console.log('Принудительное обновление кэша...'); - - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + console.log("Принудительное обновление кэша..."); + + if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { try { // Отправляем сообщение Service Worker для обновления кэша navigator.serviceWorker.controller.postMessage({ - type: 'FORCE_UPDATE_CACHE' + type: "FORCE_UPDATE_CACHE", }); - - console.log('Запрос на обновление кэша отправлен'); + + console.log("Запрос на обновление кэша отправлен"); return true; } catch (error) { - console.error('Ошибка при обновлении кэша:', error); + console.error("Ошибка при обновлении кэша:", error); return false; } } else { - console.log('Service Worker не доступен'); + console.log("Service Worker не доступен"); return false; } } // Полная очистка кэша async clearAllCache() { - console.log('Полная очистка кэша...'); - - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + console.log("Полная очистка кэша..."); + + if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { try { // Отправляем сообщение Service Worker для очистки кэша navigator.serviceWorker.controller.postMessage({ - type: 'CLEAR_ALL_CACHE' + type: "CLEAR_ALL_CACHE", }); - - console.log('Запрос на очистку кэша отправлен'); + + console.log("Запрос на очистку кэша отправлен"); return true; } catch (error) { - console.error('Ошибка при очистке кэша:', error); + console.error("Ошибка при очистке кэша:", error); return false; } } else { - console.log('Service Worker не доступен'); + console.log("Service Worker не доступен"); return false; } } @@ -379,37 +441,37 @@ class PWAManager { // Получение версии кэша async getCacheVersion() { return new Promise((resolve) => { - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { const messageChannel = new MessageChannel(); - + messageChannel.port1.onmessage = (event) => { - resolve(event.data.version || 'Неизвестно'); + resolve(event.data.version || "Неизвестно"); }; - + navigator.serviceWorker.controller.postMessage( - { type: 'GET_VERSION' }, + { type: "GET_VERSION" }, [messageChannel.port2] ); } else { - resolve('Service Worker не доступен'); + resolve("Service Worker не доступен"); } }); } // Проверка обновлений и принудительное обновление async checkForUpdates() { - console.log('Проверка обновлений...'); - - if ('serviceWorker' in navigator) { + console.log("Проверка обновлений..."); + + if ("serviceWorker" in navigator) { try { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); - console.log('Проверка обновлений завершена'); + console.log("Проверка обновлений завершена"); return true; } } catch (error) { - console.error('Ошибка при проверке обновлений:', error); + console.error("Ошибка при проверке обновлений:", error); return false; } } @@ -418,19 +480,19 @@ class PWAManager { // Настройка обработки сообщений от Service Worker setupServiceWorkerMessages() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', (event) => { - console.log('Получено сообщение от SW:', event.data); - + if ("serviceWorker" in navigator) { + navigator.serviceWorker.addEventListener("message", (event) => { + console.log("Получено сообщение от SW:", event.data); + switch (event.data.type) { - case 'CACHE_UPDATED': - console.log('Кэш обновлен до версии:', event.data.version); - this.showNotification('Кэш успешно обновлен!', 'success'); + case "CACHE_UPDATED": + console.log("Кэш обновлен до версии:", event.data.version); + this.showNotification("Кэш успешно обновлен!", "success"); break; - - case 'CACHE_CLEARED': - console.log('Кэш полностью очищен'); - this.showNotification('Кэш полностью очищен!', 'info'); + + case "CACHE_CLEARED": + console.log("Кэш полностью очищен"); + this.showNotification("Кэш полностью очищен!", "info"); break; } }); @@ -438,12 +500,12 @@ class PWAManager { } // Показ уведомления - showNotification(message, type = 'info') { + showNotification(message, type = "info") { // Создаем уведомление - const notification = document.createElement('div'); + const notification = document.createElement("div"); notification.className = `pwa-notification pwa-notification-${type}`; notification.textContent = message; - + // Стили для уведомления notification.style.cssText = ` position: fixed; @@ -459,34 +521,34 @@ class PWAManager { transform: translateX(100%); transition: transform 0.3s ease; `; - + // Цвета для разных типов уведомлений switch (type) { - case 'success': - notification.style.backgroundColor = '#28a745'; + case "success": + notification.style.backgroundColor = "#28a745"; break; - case 'error': - notification.style.backgroundColor = '#dc3545'; + case "error": + notification.style.backgroundColor = "#dc3545"; break; - case 'warning': - notification.style.backgroundColor = '#ffc107'; - notification.style.color = '#000'; + case "warning": + notification.style.backgroundColor = "#ffc107"; + notification.style.color = "#000"; break; default: - notification.style.backgroundColor = '#007bff'; + notification.style.backgroundColor = "#007bff"; } - + // Добавляем на страницу document.body.appendChild(notification); - + // Анимация появления setTimeout(() => { - notification.style.transform = 'translateX(0)'; + notification.style.transform = "translateX(0)"; }, 100); - + // Автоматическое удаление через 3 секунды setTimeout(() => { - notification.style.transform = 'translateX(100%)'; + notification.style.transform = "translateX(100%)"; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); @@ -504,7 +566,7 @@ window.PWAManager = pwaManager; // Добавляем глобальные функции для управления кэшем window.debugPWA = () => { - console.log('PWA Debug Info:', pwaManager.getPWAInfo()); + console.log("PWA Debug Info:", pwaManager.getPWAInfo()); }; // Принудительное обновление кэша @@ -529,14 +591,14 @@ window.checkUpdates = () => { // Комбинированная функция: проверка обновлений + принудительное обновление кэша window.forceUpdate = async () => { - console.log('Принудительное обновление приложения...'); - + console.log("Принудительное обновление приложения..."); + // Сначала проверяем обновления await pwaManager.checkForUpdates(); - + // Затем принудительно обновляем кэш await pwaManager.forceUpdateCache(); - + // Перезагружаем страницу setTimeout(() => { window.location.reload(); @@ -551,8 +613,35 @@ window.checkInstallability = () => { // Принудительная попытка установки window.forceInstall = () => { if (pwaManager.isMobileDevice()) { - pwaManager.showMobileInstallInstructions(); + // На мобильных устройствах пытаемся вызвать prompt() если есть deferredPrompt + if (pwaManager.deferredPrompt) { + pwaManager.installApp(); + } else { + pwaManager.showMobileInstallInstructions(); + } } else { pwaManager.showManualInstallInstructions(); } -}; \ No newline at end of file +}; + +// Показать нативный баннер установки (для мобильных устройств) +window.showInstallBanner = () => { + if (pwaManager.deferredPrompt && pwaManager.isMobileDevice()) { + pwaManager.deferredPrompt.prompt(); + pwaManager.deferredPrompt.userChoice.then((choiceResult) => { + console.log("Результат установки:", choiceResult.outcome); + if (choiceResult.outcome === "accepted") { + pwaManager.showNotification( + "Приложение успешно установлено!", + "success" + ); + } else { + pwaManager.showNotification("Установка отменена", "warning"); + } + pwaManager.deferredPrompt = null; + }); + } else { + console.log("Нативный баннер установки недоступен"); + pwaManager.showMobileInstallInstructions(); + } +}; diff --git a/public/sw.js b/public/sw.js index c1f1449..8847ac1 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,42 +1,43 @@ // Service Worker для NoteJS -const APP_VERSION = '1.0.3'; +const APP_VERSION = "1.0.5"; const CACHE_NAME = `notejs-v${APP_VERSION}`; const STATIC_CACHE_NAME = `notejs-static-v${APP_VERSION}`; -// Файлы для кэширования при установке +// Файлы для кэширования при установке (только изображения, иконки, логотипы) +// Манифест убран - не нужен для офлайн работы const STATIC_FILES = [ - '/', - '/index.html', - '/style.css', - '/manifest.json', - '/icons/icon-72x72.png', - '/icons/icon-96x96.png', - '/icons/icon-128x128.png', - '/icons/icon-144x144.png', - '/icons/icon-152x152.png', - '/icons/icon-192x192.png', - '/icons/icon-384x384.png', - '/icons/icon-512x512.png', - '/icon.svg', - '/logo.svg' + "/icons/icon-72x72.png", + "/icons/icon-96x96.png", + "/icons/icon-128x128.png", + "/icons/icon-144x144.png", + "/icons/icon-152x152.png", + "/icons/icon-192x192.png", + "/icons/icon-384x384.png", + "/icons/icon-512x512.png", + "/icons/icon-32x32.png", + "/icons/icon-16x16.png", + "/icons/icon-48x48.png", + "/icon.svg", + "/logo.svg", ]; // Установка Service Worker -self.addEventListener('install', (event) => { - console.log('[SW] Установка Service Worker'); - +self.addEventListener("install", (event) => { + console.log("[SW] Установка Service Worker"); + event.waitUntil( - caches.open(STATIC_CACHE_NAME) + caches + .open(STATIC_CACHE_NAME) .then((cache) => { - console.log('[SW] Кэширование статических файлов'); + console.log("[SW] Кэширование статических файлов"); return cache.addAll(STATIC_FILES); }) .then(() => { - console.log('[SW] Статические файлы закэшированы'); + console.log("[SW] Статические файлы закэшированы"); return self.skipWaiting(); }) .catch((error) => { - console.error('[SW] Ошибка при кэшировании статических файлов:', error); + console.error("[SW] Ошибка при кэшировании статических файлов:", error); // Продолжаем работу даже если кэширование не удалось return self.skipWaiting(); }) @@ -44,51 +45,66 @@ self.addEventListener('install', (event) => { }); // Активация Service Worker -self.addEventListener('activate', (event) => { - console.log('[SW] Активация Service Worker'); - +self.addEventListener("activate", (event) => { + console.log("[SW] Активация Service Worker"); + event.waitUntil( - caches.keys() + caches + .keys() .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { // Удаляем старые кэши if (cacheName !== STATIC_CACHE_NAME) { - console.log('[SW] Удаление старого кэша:', cacheName); + console.log("[SW] Удаление старого кэша:", cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { - console.log('[SW] Service Worker активирован'); + console.log("[SW] Service Worker активирован"); return self.clients.claim(); }) ); }); // Перехват запросов -self.addEventListener('fetch', (event) => { +self.addEventListener("fetch", (event) => { const { request } = event; const url = new URL(request.url); - + // Пропускаем запросы к API и загрузкам - if (url.pathname.startsWith('/api/') || - url.pathname.startsWith('/uploads/') || - url.pathname.startsWith('/database/')) { + if ( + url.pathname.startsWith("/api/") || + url.pathname.startsWith("/uploads/") || + url.pathname.startsWith("/database/") + ) { return; } - + // Пропускаем запросы к внешним ресурсам if (url.origin !== location.origin) { return; } - - // Обрабатываем только GET запросы - if (request.method === 'GET') { - event.respondWith( - handleRequest(request) - ); + + // Пропускаем HTML файлы и CSS - не кэшируем их + if ( + url.pathname.endsWith(".html") || + url.pathname.endsWith(".css") || + url.pathname === "/" || + url.pathname === "/index.html" || + url.pathname === "/login.html" || + url.pathname === "/register.html" || + url.pathname === "/notes.html" || + url.pathname === "/profile.html" + ) { + return; + } + + // Обрабатываем только GET запросы для изображений, иконок и манифеста + if (request.method === "GET") { + event.respondWith(handleRequest(request)); } }); @@ -96,134 +112,124 @@ async function handleRequest(request) { try { // Сначала пытаемся получить из кэша const cachedResponse = await caches.match(request); - + if (cachedResponse) { - console.log('[SW] Запрос из кэша:', request.url); + console.log("[SW] Запрос из кэша:", request.url); return cachedResponse; } - + // Если нет в кэше, загружаем из сети - console.log('[SW] Запрос к сети:', request.url); + console.log("[SW] Запрос к сети:", request.url); const networkResponse = await fetch(request); - - // Кэшируем успешные ответы - if (networkResponse.ok) { + + // Кэшируем только изображения и иконки (без манифеста) + if (networkResponse.ok && shouldCache(request)) { const cache = await caches.open(STATIC_CACHE_NAME); cache.put(request, networkResponse.clone()); } - + return networkResponse; - } catch (error) { - console.error('[SW] Ошибка при обработке запроса:', error); - - // Fallback для HTML страниц - if (request.headers.get('accept')?.includes('text/html')) { - const fallbackResponse = await caches.match('/index.html'); - if (fallbackResponse) { - return fallbackResponse; - } - } - - // Fallback для иконок - if (request.url.includes('/icons/')) { - const fallbackIcon = await caches.match('/icons/icon-192x192.png'); - if (fallbackIcon) { - return fallbackIcon; - } - } - - // Fallback для CSS - if (request.url.includes('/style.css')) { - const fallbackCSS = await caches.match('/style.css'); - if (fallbackCSS) { - return fallbackCSS; - } - } - + console.error("[SW] Ошибка при обработке запроса:", error); + + // НЕ предоставляем fallback - приложение работает только онлайн throw error; } } +// Функция для определения, нужно ли кэшировать файл +function shouldCache(request) { + const url = new URL(request.url); + + // Кэшируем только изображения, иконки и логотипы (без манифеста для офлайн работы) + return ( + url.pathname.includes("/icons/") || + url.pathname.endsWith(".png") || + url.pathname.endsWith(".jpg") || + url.pathname.endsWith(".jpeg") || + url.pathname.endsWith(".gif") || + url.pathname.endsWith(".webp") || + url.pathname.endsWith(".svg") + ); +} + // Обработка сообщений от основного потока -self.addEventListener('message', (event) => { - console.log('[SW] Получено сообщение:', event.data); - - if (event.data && event.data.type === 'SKIP_WAITING') { +self.addEventListener("message", (event) => { + console.log("[SW] Получено сообщение:", event.data); + + if (event.data && event.data.type === "SKIP_WAITING") { self.skipWaiting(); } - - if (event.data && event.data.type === 'GET_VERSION') { + + if (event.data && event.data.type === "GET_VERSION") { event.ports[0].postMessage({ version: CACHE_NAME }); } - - if (event.data && event.data.type === 'FORCE_UPDATE_CACHE') { + + if (event.data && event.data.type === "FORCE_UPDATE_CACHE") { forceUpdateCache(); } - - if (event.data && event.data.type === 'CLEAR_ALL_CACHE') { + + if (event.data && event.data.type === "CLEAR_ALL_CACHE") { clearAllCache(); } }); // Функция принудительного обновления кэша async function forceUpdateCache() { - console.log('[SW] Принудительное обновление кэша...'); - + console.log("[SW] Принудительное обновление кэша..."); + try { // Удаляем все старые кэши const cacheNames = await caches.keys(); await Promise.all( - cacheNames.map(cacheName => { - console.log('[SW] Удаление кэша:', cacheName); + cacheNames.map((cacheName) => { + console.log("[SW] Удаление кэша:", cacheName); return caches.delete(cacheName); }) ); - + // Создаем новый кэш с актуальной версией const newCache = await caches.open(STATIC_CACHE_NAME); await newCache.addAll(STATIC_FILES); - - console.log('[SW] Кэш успешно обновлен до версии:', APP_VERSION); - + + console.log("[SW] Кэш успешно обновлен до версии:", APP_VERSION); + // Уведомляем клиентов об обновлении const clients = await self.clients.matchAll(); - clients.forEach(client => { + clients.forEach((client) => { client.postMessage({ - type: 'CACHE_UPDATED', - version: APP_VERSION + type: "CACHE_UPDATED", + version: APP_VERSION, }); }); - } catch (error) { - console.error('[SW] Ошибка при обновлении кэша:', error); + console.error("[SW] Ошибка при обновлении кэша:", error); } } // Функция полной очистки кэша async function clearAllCache() { - console.log('[SW] Полная очистка кэша...'); - + console.log("[SW] Полная очистка кэша..."); + try { const cacheNames = await caches.keys(); await Promise.all( - cacheNames.map(cacheName => { - console.log('[SW] Удаление кэша:', cacheName); + cacheNames.map((cacheName) => { + console.log("[SW] Удаление кэша:", cacheName); return caches.delete(cacheName); }) ); - - console.log('[SW] Весь кэш очищен'); - + + console.log("[SW] Весь кэш очищен"); + // Уведомляем клиентов об очистке const clients = await self.clients.matchAll(); - clients.forEach(client => { + clients.forEach((client) => { client.postMessage({ - type: 'CACHE_CLEARED' + type: "CACHE_CLEARED", }); }); - } catch (error) { - console.error('[SW] Ошибка при очистке кэша:', error); + console.error("[SW] Ошибка при очистке кэша:", error); } -} \ No newline at end of file +} diff --git a/public/test-pwa.html b/public/test-pwa.html index 7b72b78..a4bdbb9 100644 --- a/public/test-pwa.html +++ b/public/test-pwa.html @@ -1,10 +1,10 @@ - - - + + + Тест PWA - NoteJS - + @@ -12,206 +12,276 @@ - + - - - - + + + + - + - - + +
-

🔧 Тест PWA для NoteJS

- -
-
Проверяем требования PWA...
-
- -
- - - -
- -
-

Отладочная информация:

-
-
+

🔧 Тест PWA для NoteJS

+ +
+
Проверяем требования PWA...
+
+ +
+ + + +
+ +
+

Отладочная информация:

+
+
- + - +