Обновлены инструкции и функциональность PWA

- Изменена структура инструкций по тестированию PWA, добавлена диагностическая страница для отладки.
- Обновлен manifest.json с добавлением поля "id".
- Реализована задержка при отображении кнопки установки для улучшения пользовательского опыта.
- Добавлены функции для проверки возможности установки PWA и отображения инструкций для мобильных устройств.
This commit is contained in:
Fovway 2025-10-20 09:42:36 +07:00
parent 95401328c4
commit d6dc1d76a0
4 changed files with 535 additions and 7 deletions

View File

@ -22,17 +22,22 @@
## Как протестировать
### 1. Откройте тестовую страницу для мобильных устройств
### 1. Откройте диагностическую страницу PWA
```
http://localhost:3000/pwa-debug.html
```
### 2. Откройте тестовую страницу для мобильных устройств
```
http://localhost:3000/mobile-pwa-test.html
```
### 2. Откройте обычную тестовую страницу
### 3. Откройте обычную тестовую страницу
```
http://localhost:3000/test-pwa.html
```
### 3. Проверьте требования PWA
### 4. Проверьте требования PWA
На мобильной тестовой странице автоматически проверяются все требования:
- ✅ HTTPS или localhost
- ✅ Service Worker
@ -40,12 +45,12 @@ http://localhost:3000/test-pwa.html
- ✅ Иконки
- ❌ Уже установлено (должно быть красным, если не установлено)
### 4. Установка приложения
### 5. Установка приложения
- Если все проверки пройдены, появится кнопка "Установить приложение"
- Нажмите на неё для установки PWA
- Следуйте инструкциям браузера или используйте инструкции на странице
### 5. Проверка в разных браузерах
### 6. Проверка в разных браузерах
#### Chrome/Edge:
- Откройте DevTools (F12)
@ -96,6 +101,9 @@ http://localhost:3000/test-pwa.html
- Улучшенная проверка установки PWA
- Поддержка различных режимов отображения
- **Кнопка установки показывается только на мобильных устройствах**
- Принудительная проверка возможности установки
- Специальные инструкции для Android и iOS
- Диагностическая страница для отладки PWA
## Возможные проблемы и решения
@ -151,8 +159,22 @@ window.debugPWA();
// Проверить Service Worker
navigator.serviceWorker.getRegistrations().then(console.log);
// Проверить возможность установки
window.checkInstallability();
// Принудительная попытка установки
window.forceInstall();
```
### Диагностическая страница
Используйте `http://localhost:3000/pwa-debug.html` для:
- Детальной диагностики всех требований PWA
- Проверки загрузки манифеста и Service Worker
- Отображения информации о браузере и устройстве
- Просмотра ошибок консоли
- Тестирования установки PWA
### Lighthouse
Запустите аудит Lighthouse в Chrome DevTools:
1. Откройте DevTools (F12)
@ -166,6 +188,7 @@ navigator.serviceWorker.getRegistrations().then(console.log);
- ✅ `http://localhost:3000/sw.js` - должен возвращать JavaScript
- ✅ `http://localhost:3000/icons/icon-192x192.png` - должен возвращать PNG
- ✅ `http://localhost:3000/icons/icon-512x512.png` - должен возвращать PNG
- ✅ `http://localhost:3000/pwa-debug.html` - диагностическая страница PWA
- ✅ `http://localhost:3000/mobile-pwa-test.html` - мобильная тестовая страница
- ✅ `http://localhost:3000/test-pwa.html` - обычная тестовая страница

View File

@ -9,6 +9,7 @@
"orientation": "portrait-primary",
"scope": "/",
"lang": "ru",
"id": "/",
"categories": ["productivity", "utilities"],
"screenshots": [
{

411
public/pwa-debug.html Normal file
View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Диагностика PWA - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Диагностика PWA для NoteJS" />
<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" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<style>
.debug-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.debug-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.debug-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e9ecef;
}
.debug-item:last-child {
border-bottom: none;
}
.status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.status.pass {
background: #d4edda;
color: #155724;
}
.status.fail {
background: #f8d7da;
color: #721c24;
}
.status.warning {
background: #fff3cd;
color: #856404;
}
.debug-code {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
}
.install-button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin: 10px 0;
}
.install-button:hover {
background: #0056b3;
}
.install-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="debug-container">
<h1>🔍 Диагностика PWA</h1>
<div class="debug-section">
<h3>Основные требования PWA</h3>
<div id="basic-checks">
<div class="debug-item">
<span>HTTPS или localhost</span>
<span class="status" id="https-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Service Worker</span>
<span class="status" id="sw-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Manifest</span>
<span class="status" id="manifest-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Иконки</span>
<span class="status" id="icons-check">Проверка...</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Детальная диагностика</h3>
<div id="detailed-checks">
<div class="debug-item">
<span>Manifest загружается</span>
<span class="status" id="manifest-load-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Service Worker регистрируется</span>
<span class="status" id="sw-register-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Иконки доступны</span>
<span class="status" id="icons-available-check">Проверка...</span>
</div>
<div class="debug-item">
<span>beforeinstallprompt событие</span>
<span class="status" id="install-prompt-check">Проверка...</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Установка приложения</h3>
<button id="install-button" class="install-button" disabled>
Установить приложение
</button>
<div id="install-status"></div>
</div>
<div class="debug-section">
<h3>Информация о манифесте</h3>
<div id="manifest-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Информация о Service Worker</h3>
<div id="sw-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Информация о браузере</h3>
<div id="browser-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Ошибки консоли</h3>
<div id="console-errors" class="debug-code">Нет ошибок</div>
</div>
</div>
<script>
let deferredPrompt;
let consoleErrors = [];
// Перехватываем ошибки консоли
const originalError = console.error;
console.error = function(...args) {
consoleErrors.push(args.join(' '));
originalError.apply(console, args);
};
// Проверка основных требований PWA
function checkBasicRequirements() {
// HTTPS или localhost
const isSecure = location.protocol === 'https:' || location.hostname === 'localhost';
document.getElementById('https-check').textContent = isSecure ? '✅ Да' : '❌ Нет';
document.getElementById('https-check').className = `status ${isSecure ? 'pass' : 'fail'}`;
// Service Worker
const hasSW = 'serviceWorker' in navigator;
document.getElementById('sw-check').textContent = hasSW ? '✅ Да' : '❌ Нет';
document.getElementById('sw-check').className = `status ${hasSW ? 'pass' : 'fail'}`;
// Manifest
const hasManifest = document.querySelector('link[rel="manifest"]') !== null;
document.getElementById('manifest-check').textContent = hasManifest ? '✅ Да' : '❌ Нет';
document.getElementById('manifest-check').className = `status ${hasManifest ? 'pass' : 'fail'}`;
// Иконки
const hasIcons = document.querySelector('link[rel="icon"]') !== null;
document.getElementById('icons-check').textContent = hasIcons ? '✅ Да' : '❌ Нет';
document.getElementById('icons-check').className = `status ${hasIcons ? 'pass' : 'fail'}`;
}
// Детальная диагностика
async function detailedDiagnostics() {
// Проверка загрузки манифеста
try {
const manifestResponse = await fetch('/manifest.json');
const manifest = await manifestResponse.json();
document.getElementById('manifest-load-check').textContent = '✅ Загружен';
document.getElementById('manifest-load-check').className = 'status pass';
// Отображаем информацию о манифесте
document.getElementById('manifest-info').textContent = JSON.stringify(manifest, null, 2);
} catch (error) {
document.getElementById('manifest-load-check').textContent = '❌ Ошибка загрузки';
document.getElementById('manifest-load-check').className = 'status fail';
document.getElementById('manifest-info').textContent = `Ошибка: ${error.message}`;
}
// Проверка регистрации Service Worker
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
document.getElementById('sw-register-check').textContent = '✅ Зарегистрирован';
document.getElementById('sw-register-check').className = 'status pass';
// Отображаем информацию о Service Worker
document.getElementById('sw-info').textContent = JSON.stringify({
scope: registration.scope,
state: registration.active ? registration.active.state : 'не активен',
scriptURL: registration.active ? registration.active.scriptURL : 'не доступен'
}, null, 2);
} else {
document.getElementById('sw-register-check').textContent = '❌ Не зарегистрирован';
document.getElementById('sw-register-check').className = 'status fail';
}
} catch (error) {
document.getElementById('sw-register-check').textContent = '❌ Ошибка';
document.getElementById('sw-register-check').className = 'status fail';
}
}
// Проверка доступности иконок
const iconSizes = ['192x192', '512x512'];
let iconsAvailable = 0;
for (const size of iconSizes) {
try {
const response = await fetch(`/icons/icon-${size}.png`);
if (response.ok) {
iconsAvailable++;
}
} catch (error) {
console.error(`Ошибка загрузки иконки ${size}:`, error);
}
}
if (iconsAvailable === iconSizes.length) {
document.getElementById('icons-available-check').textContent = '✅ Все доступны';
document.getElementById('icons-available-check').className = 'status pass';
} else {
document.getElementById('icons-available-check').textContent = `⚠️ ${iconsAvailable}/${iconSizes.length} доступны`;
document.getElementById('icons-available-check').className = 'status warning';
}
// Информация о браузере
document.getElementById('browser-info').textContent = JSON.stringify({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
onLine: navigator.onLine,
cookieEnabled: navigator.cookieEnabled,
maxTouchPoints: navigator.maxTouchPoints,
displayMode: window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser',
standalone: window.navigator.standalone
}, null, 2);
}
// Регистрация Service Worker
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW зарегистрирован успешно:', registration.scope);
})
.catch((error) => {
console.log('Ошибка регистрации SW:', error);
});
}
}
// Обработка установки
window.addEventListener('beforeinstallprompt', (e) => {
console.log('beforeinstallprompt событие получено');
e.preventDefault();
deferredPrompt = e;
document.getElementById('install-prompt-check').textContent = '✅ Получено';
document.getElementById('install-prompt-check').className = 'status pass';
const installButton = document.getElementById('install-button');
installButton.disabled = false;
installButton.textContent = '📱 Установить приложение';
});
// Обработка успешной установки
window.addEventListener('appinstalled', () => {
console.log('PWA установлено успешно');
document.getElementById('install-status').innerHTML =
'<div class="success-message">✅ Приложение успешно установлено!</div>';
const installButton = document.getElementById('install-button');
installButton.disabled = true;
installButton.textContent = 'Приложение установлено';
});
// Установка приложения
document.getElementById('install-button').addEventListener('click', () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
console.log('Результат установки:', choiceResult.outcome);
if (choiceResult.outcome === 'accepted') {
document.getElementById('install-status').innerHTML =
'<div class="success-message">✅ Пользователь принял установку</div>';
} else {
document.getElementById('install-status').innerHTML =
'<div class="error-message">⚠️ Пользователь отклонил установку</div>';
}
deferredPrompt = null;
});
} else {
document.getElementById('install-status').innerHTML =
'<div class="error-message">❌ Установка недоступна. Попробуйте использовать меню браузера.</div>';
}
});
// Обновление ошибок консоли
function updateConsoleErrors() {
if (consoleErrors.length > 0) {
document.getElementById('console-errors').textContent = consoleErrors.join('\n');
} else {
document.getElementById('console-errors').textContent = 'Нет ошибок';
}
}
// Инициализация
document.addEventListener('DOMContentLoaded', () => {
checkBasicRequirements();
detailedDiagnostics();
registerServiceWorker();
// Обновляем ошибки каждые 2 секунды
setInterval(updateConsoleErrors, 2000);
});
</script>
</body>
</html>

View File

@ -66,7 +66,11 @@ class PWAManager {
console.log('beforeinstallprompt событие получено');
e.preventDefault();
this.deferredPrompt = e;
this.showInstallButton();
// Показываем кнопку установки с задержкой для лучшего UX
setTimeout(() => {
this.showInstallButton();
}, 1000);
});
}
@ -101,7 +105,8 @@ class PWAManager {
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;
window.matchMedia('(max-width: 768px)').matches ||
/Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
}
// Проверка на мобильный Safari
@ -178,6 +183,10 @@ class PWAManager {
console.log('Результат установки:', choiceResult.outcome);
if (choiceResult.outcome === 'accepted') {
console.log('Пользователь установил приложение');
this.showNotification('Приложение успешно установлено!', 'success');
} else {
console.log('Пользователь отклонил установку');
this.showNotification('Установка отменена', 'warning');
}
this.deferredPrompt = null;
this.removeInstallButton();
@ -188,6 +197,41 @@ class PWAManager {
}
}
// Принудительная проверка возможности установки
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 = `
@ -210,6 +254,41 @@ class PWAManager {
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');
@ -462,4 +541,18 @@ window.forceUpdate = async () => {
setTimeout(() => {
window.location.reload();
}, 1000);
};
// Проверка возможности установки PWA
window.checkInstallability = () => {
return pwaManager.checkInstallability();
};
// Принудительная попытка установки
window.forceInstall = () => {
if (pwaManager.isMobileDevice()) {
pwaManager.showMobileInstallInstructions();
} else {
pwaManager.showManualInstallInstructions();
}
};