- Оптимизированы мета-теги в index.html и test-pwa.html для лучшей поддержки PWA. - Улучшена структура кода с использованием многострочных атрибутов для мета-тегов. - Обновлен сервисный работник для более эффективного кэширования и обработки запросов. - Добавлены новые функции в pwa.js для управления установкой и обновлением PWA.
648 lines
22 KiB
JavaScript
648 lines
22 KiB
JavaScript
// PWA Service Worker Registration и установка
|
||
class PWAManager {
|
||
constructor() {
|
||
this.deferredPrompt = null;
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
console.log("PWA Manager инициализирован");
|
||
this.registerServiceWorker();
|
||
this.setupInstallPrompt();
|
||
this.setupAppInstalled();
|
||
this.checkPWARequirements();
|
||
this.setupServiceWorkerMessages();
|
||
}
|
||
|
||
// Проверка требований 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
|
||
);
|
||
}
|
||
|
||
// Регистрация Service Worker
|
||
registerServiceWorker() {
|
||
if ("serviceWorker" in navigator) {
|
||
window.addEventListener("load", () => {
|
||
navigator.serviceWorker
|
||
.register("/sw.js")
|
||
.then((registration) => {
|
||
console.log("SW зарегистрирован успешно:", registration.scope);
|
||
|
||
// Проверяем обновления
|
||
registration.addEventListener("updatefound", () => {
|
||
const newWorker = registration.installing;
|
||
newWorker.addEventListener("statechange", () => {
|
||
if (
|
||
newWorker.state === "installed" &&
|
||
navigator.serviceWorker.controller
|
||
) {
|
||
this.showUpdateNotification();
|
||
}
|
||
});
|
||
});
|
||
})
|
||
.catch((error) => {
|
||
console.log("Ошибка регистрации SW:", error);
|
||
});
|
||
});
|
||
} else {
|
||
console.log("Service Worker не поддерживается");
|
||
}
|
||
}
|
||
|
||
// Показ уведомления об обновлении
|
||
showUpdateNotification() {
|
||
if (confirm("Доступна новая версия приложения. Обновить?")) {
|
||
if (navigator.serviceWorker.controller) {
|
||
navigator.serviceWorker.controller.postMessage({
|
||
type: "SKIP_WAITING",
|
||
});
|
||
}
|
||
window.location.reload();
|
||
}
|
||
}
|
||
|
||
// Настройка промпта установки
|
||
setupInstallPrompt() {
|
||
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();
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
// Показ кнопки установки
|
||
showInstallButton() {
|
||
console.log("Попытка показать кнопку установки");
|
||
|
||
// Проверяем, не установлено ли уже приложение
|
||
if (this.isPWAInstalled()) {
|
||
console.log("Приложение уже установлено");
|
||
return;
|
||
}
|
||
|
||
// Показываем кнопку только на десктопе (на мобильных используем нативный баннер)
|
||
if (this.isMobileDevice()) {
|
||
console.log(
|
||
"Мобильное устройство - используем нативный баннер установки"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Проверяем, поддерживает ли браузер установку PWA
|
||
if (!this.deferredPrompt) {
|
||
console.log("Установка PWA не поддерживается в этом браузере");
|
||
return;
|
||
}
|
||
|
||
const installButton = this.createInstallButton();
|
||
this.addInstallButtonToPage(installButton);
|
||
}
|
||
|
||
// Проверка на мобильное устройство
|
||
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)
|
||
);
|
||
}
|
||
|
||
// Проверка на мобильный Safari
|
||
isMobileSafari() {
|
||
const ua = navigator.userAgent;
|
||
return (
|
||
/iPad|iPhone|iPod/.test(ua) &&
|
||
/Safari/.test(ua) &&
|
||
!/CriOS|FxiOS|OPiOS|mercury/.test(ua)
|
||
);
|
||
}
|
||
|
||
// Создание кнопки установки
|
||
createInstallButton() {
|
||
const installButton = document.createElement("button");
|
||
|
||
// Разный текст для разных браузеров
|
||
if (this.isMobileSafari()) {
|
||
installButton.textContent = "📱 Добавить на главный экран";
|
||
} else {
|
||
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", () => {
|
||
this.installApp();
|
||
});
|
||
|
||
return installButton;
|
||
}
|
||
|
||
// Добавление кнопки на страницу
|
||
addInstallButtonToPage(installButton) {
|
||
// Удаляем существующую кнопку, если есть
|
||
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");
|
||
|
||
if (authLink) {
|
||
authLink.appendChild(installButton);
|
||
console.log("Кнопка установки добавлена в auth-link");
|
||
} else if (footer) {
|
||
footer.insertBefore(installButton, footer.firstChild);
|
||
console.log("Кнопка установки добавлена в footer");
|
||
} else if (container) {
|
||
container.appendChild(installButton);
|
||
console.log("Кнопка установки добавлена в container");
|
||
} else {
|
||
document.body.appendChild(installButton);
|
||
console.log("Кнопка установки добавлена в body");
|
||
}
|
||
}
|
||
|
||
// Установка приложения
|
||
installApp() {
|
||
console.log("Попытка установки приложения");
|
||
|
||
if (this.isMobileSafari()) {
|
||
// Для iOS Safari показываем инструкции
|
||
this.showSafariInstructions();
|
||
return;
|
||
}
|
||
|
||
if (this.deferredPrompt) {
|
||
// На мобильных устройствах вызываем 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 не доступен");
|
||
this.showManualInstallInstructions();
|
||
}
|
||
}
|
||
|
||
// Принудительная проверка возможности установки
|
||
async checkInstallability() {
|
||
if (!this.isMobileDevice()) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем все требования 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,
|
||
};
|
||
|
||
const allRequirementsMet = Object.values(requirements).every((req) => req);
|
||
|
||
if (!allRequirementsMet) {
|
||
console.log("Не все требования PWA выполнены:", requirements);
|
||
return false;
|
||
}
|
||
|
||
// Проверяем, есть ли deferredPrompt
|
||
if (this.deferredPrompt) {
|
||
return true;
|
||
}
|
||
|
||
// Для мобильных устройств без deferredPrompt показываем инструкции
|
||
if (this.isMobileDevice()) {
|
||
this.showMobileInstallInstructions();
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Показать инструкции для Safari
|
||
showSafariInstructions() {
|
||
const instructions = `
|
||
Для установки приложения на iOS:
|
||
1. Нажмите кнопку "Поделиться" (□↗) внизу экрана
|
||
2. Выберите "На экран Домой"
|
||
3. Нажмите "Добавить"
|
||
`;
|
||
alert(instructions);
|
||
}
|
||
|
||
// Показать инструкции для ручной установки
|
||
showManualInstallInstructions() {
|
||
const instructions = `
|
||
Для установки приложения:
|
||
1. Откройте меню браузера (⋮ или ☰)
|
||
2. Найдите "Установить приложение" или "Добавить на главный экран"
|
||
3. Следуйте инструкциям браузера
|
||
`;
|
||
alert(instructions);
|
||
}
|
||
|
||
// Показать инструкции для мобильных устройств
|
||
showMobileInstallInstructions() {
|
||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||
|
||
let instructions = "";
|
||
|
||
if (isAndroid) {
|
||
instructions = `
|
||
Для установки приложения на Android:
|
||
1. Нажмите на меню браузера (⋮)
|
||
2. Выберите "Установить приложение" или "Добавить на главный экран"
|
||
3. Подтвердите установку
|
||
|
||
Или нажмите на иконку установки в адресной строке, если она появилась.
|
||
`;
|
||
} else if (isIOS) {
|
||
instructions = `
|
||
Для установки приложения на iOS:
|
||
1. Нажмите кнопку "Поделиться" (□↗) внизу экрана
|
||
2. Выберите "На экран Домой"
|
||
3. Нажмите "Добавить"
|
||
`;
|
||
} else {
|
||
instructions = `
|
||
Для установки приложения:
|
||
1. Откройте меню браузера
|
||
2. Найдите "Установить приложение" или "Добавить на главный экран"
|
||
3. Следуйте инструкциям браузера
|
||
`;
|
||
}
|
||
|
||
alert(instructions);
|
||
}
|
||
|
||
// Удаление кнопки установки
|
||
removeInstallButton() {
|
||
const installButton = document.getElementById("pwa-install-button");
|
||
if (installButton) {
|
||
installButton.remove();
|
||
console.log("Кнопка установки удалена");
|
||
}
|
||
}
|
||
|
||
// Обработка успешной установки
|
||
setupAppInstalled() {
|
||
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
|
||
);
|
||
}
|
||
|
||
// Получение информации о PWA
|
||
getPWAInfo() {
|
||
return {
|
||
isInstalled: this.isPWAInstalled(),
|
||
isOnline: navigator.onLine,
|
||
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",
|
||
};
|
||
}
|
||
|
||
// Принудительное обновление кэша
|
||
async forceUpdateCache() {
|
||
console.log("Принудительное обновление кэша...");
|
||
|
||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||
try {
|
||
// Отправляем сообщение Service Worker для обновления кэша
|
||
navigator.serviceWorker.controller.postMessage({
|
||
type: "FORCE_UPDATE_CACHE",
|
||
});
|
||
|
||
console.log("Запрос на обновление кэша отправлен");
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Ошибка при обновлении кэша:", error);
|
||
return false;
|
||
}
|
||
} else {
|
||
console.log("Service Worker не доступен");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Полная очистка кэша
|
||
async clearAllCache() {
|
||
console.log("Полная очистка кэша...");
|
||
|
||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||
try {
|
||
// Отправляем сообщение Service Worker для очистки кэша
|
||
navigator.serviceWorker.controller.postMessage({
|
||
type: "CLEAR_ALL_CACHE",
|
||
});
|
||
|
||
console.log("Запрос на очистку кэша отправлен");
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Ошибка при очистке кэша:", error);
|
||
return false;
|
||
}
|
||
} else {
|
||
console.log("Service Worker не доступен");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Получение версии кэша
|
||
async getCacheVersion() {
|
||
return new Promise((resolve) => {
|
||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||
const messageChannel = new MessageChannel();
|
||
|
||
messageChannel.port1.onmessage = (event) => {
|
||
resolve(event.data.version || "Неизвестно");
|
||
};
|
||
|
||
navigator.serviceWorker.controller.postMessage(
|
||
{ type: "GET_VERSION" },
|
||
[messageChannel.port2]
|
||
);
|
||
} else {
|
||
resolve("Service Worker не доступен");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Проверка обновлений и принудительное обновление
|
||
async checkForUpdates() {
|
||
console.log("Проверка обновлений...");
|
||
|
||
if ("serviceWorker" in navigator) {
|
||
try {
|
||
const registration = await navigator.serviceWorker.getRegistration();
|
||
if (registration) {
|
||
await registration.update();
|
||
console.log("Проверка обновлений завершена");
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при проверке обновлений:", error);
|
||
return false;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Настройка обработки сообщений от Service Worker
|
||
setupServiceWorkerMessages() {
|
||
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");
|
||
break;
|
||
|
||
case "CACHE_CLEARED":
|
||
console.log("Кэш полностью очищен");
|
||
this.showNotification("Кэш полностью очищен!", "info");
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Показ уведомления
|
||
showNotification(message, type = "info") {
|
||
// Создаем уведомление
|
||
const notification = document.createElement("div");
|
||
notification.className = `pwa-notification pwa-notification-${type}`;
|
||
notification.textContent = message;
|
||
|
||
// Стили для уведомления
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-weight: bold;
|
||
z-index: 10000;
|
||
max-width: 300px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
transform: translateX(100%);
|
||
transition: transform 0.3s ease;
|
||
`;
|
||
|
||
// Цвета для разных типов уведомлений
|
||
switch (type) {
|
||
case "success":
|
||
notification.style.backgroundColor = "#28a745";
|
||
break;
|
||
case "error":
|
||
notification.style.backgroundColor = "#dc3545";
|
||
break;
|
||
case "warning":
|
||
notification.style.backgroundColor = "#ffc107";
|
||
notification.style.color = "#000";
|
||
break;
|
||
default:
|
||
notification.style.backgroundColor = "#007bff";
|
||
}
|
||
|
||
// Добавляем на страницу
|
||
document.body.appendChild(notification);
|
||
|
||
// Анимация появления
|
||
setTimeout(() => {
|
||
notification.style.transform = "translateX(0)";
|
||
}, 100);
|
||
|
||
// Автоматическое удаление через 3 секунды
|
||
setTimeout(() => {
|
||
notification.style.transform = "translateX(100%)";
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.parentNode.removeChild(notification);
|
||
}
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
}
|
||
|
||
// Инициализация PWA Manager
|
||
const pwaManager = new PWAManager();
|
||
|
||
// Экспорт для использования в других скриптах
|
||
window.PWAManager = pwaManager;
|
||
|
||
// Добавляем глобальные функции для управления кэшем
|
||
window.debugPWA = () => {
|
||
console.log("PWA Debug Info:", pwaManager.getPWAInfo());
|
||
};
|
||
|
||
// Принудительное обновление кэша
|
||
window.updateCache = () => {
|
||
return pwaManager.forceUpdateCache();
|
||
};
|
||
|
||
// Полная очистка кэша
|
||
window.clearCache = () => {
|
||
return pwaManager.clearAllCache();
|
||
};
|
||
|
||
// Получение версии кэша
|
||
window.getCacheVersion = () => {
|
||
return pwaManager.getCacheVersion();
|
||
};
|
||
|
||
// Проверка обновлений
|
||
window.checkUpdates = () => {
|
||
return pwaManager.checkForUpdates();
|
||
};
|
||
|
||
// Комбинированная функция: проверка обновлений + принудительное обновление кэша
|
||
window.forceUpdate = async () => {
|
||
console.log("Принудительное обновление приложения...");
|
||
|
||
// Сначала проверяем обновления
|
||
await pwaManager.checkForUpdates();
|
||
|
||
// Затем принудительно обновляем кэш
|
||
await pwaManager.forceUpdateCache();
|
||
|
||
// Перезагружаем страницу
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 1000);
|
||
};
|
||
|
||
// Проверка возможности установки PWA
|
||
window.checkInstallability = () => {
|
||
return pwaManager.checkInstallability();
|
||
};
|
||
|
||
// Принудительная попытка установки
|
||
window.forceInstall = () => {
|
||
if (pwaManager.isMobileDevice()) {
|
||
// На мобильных устройствах пытаемся вызвать prompt() если есть deferredPrompt
|
||
if (pwaManager.deferredPrompt) {
|
||
pwaManager.installApp();
|
||
} else {
|
||
pwaManager.showMobileInstallInstructions();
|
||
}
|
||
} else {
|
||
pwaManager.showManualInstallInstructions();
|
||
}
|
||
};
|
||
|
||
// Показать нативный баннер установки (для мобильных устройств)
|
||
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();
|
||
}
|
||
};
|