HTML для PWA

Сложность: Продвинутый

HTML для Progressive Web Apps

Использование HTML для создания прогрессивных веб-приложений.

1. Базовая структура PWA

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="theme-color" content="#4a90e2">
    <meta name="description" content="Мое прогрессивное веб-приложение">
    
    <!-- PWA мета-теги -->
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <meta name="apple-mobile-web-app-title" content="Мое PWA">
    
    <link rel="manifest" href="/manifest.json">
    <link rel="icon" type="image/png" href="/icons/icon-192x192.png">
    <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
    
    <title>Мое PWA приложение</title>
    
    <style>
        :root {
            --primary-color: #4a90e2;
            --secondary-color: #2c3e50;
            --background-color: #f5f5f5;
            --text-color: #333;
        }
        
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: var(--background-color);
            color: var(--text-color);
            line-height: 1.6;
        }
        
        .app-shell {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
        }
        
        .app-header {
            background: var(--primary-color);
            color: white;
            padding: 1rem;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            position: sticky;
            top: 0;
            z-index: 1000;
        }
        
        .app-main {
            flex: 1;
            padding: 1rem;
            max-width: 1200px;
            margin: 0 auto;
            width: 100%;
        }
        
        .app-footer {
            background: var(--secondary-color);
            color: white;
            padding: 1rem;
            text-align: center;
        }
        
        .offline-indicator {
            background: #e74c3c;
            color: white;
            padding: 0.5rem;
            text-align: center;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            z-index: 1001;
            display: none;
        }
        
        .install-prompt {
            background: #2ecc71;
            color: white;
            padding: 1rem;
            margin: 1rem 0;
            border-radius: 4px;
            display: none;
        }
    </style>
</head>
<body>
    <div class="offline-indicator" id="offlineIndicator">
        ? Вы в оффлайн режиме
    </div>
    
    <div class="app-shell">
        <header class="app-header">
            <h1>Мое PWA приложение</h1>
            <nav>
                <button id="menuBtn">☰ Меню</button>
            </nav>
        </header>
        
        <main class="app-main">
            <div class="install-prompt" id="installPrompt">
                <p>Установите приложение для лучшего опыта!</p>
                <button id="installBtn">Установить</button>
                <button id="dismissBtn">Позже</button>
            </div>
            
            <section id="mainContent">
                <h2>Добро пожаловать!</h2>
                <p>Это прогрессивное веб-приложение.</p>
            </section>
        </main>
        
        <footer class="app-footer">
            <p>© 2024 Мое PWA приложение</p>
        </footer>
    </div>
    
    <script>
        // Регистрация Service Worker
        if ("serviceWorker" in navigator) {
            window.addEventListener("load", () => {
                navigator.serviceWorker.register("/sw.js")
                    .then(registration => {
                        console.log("SW registered: ", registration);
                    })
                    .catch(error => {
                        console.log("SW registration failed: ", error);
                    });
            });
        }
        
        // Отслеживание онлайн/оффлайн статуса
        window.addEventListener("online", () => {
            document.getElementById("offlineIndicator").style.display = "none";
            showMessage("Соединение восстановлено", "success");
        });
        
        window.addEventListener("offline", () => {
            document.getElementById("offlineIndicator").style.display = "block";
            showMessage("Вы в оффлайн режиме", "warning");
        });
        
        // Prompt для установки
        let deferredPrompt;
        
        window.addEventListener("beforeinstallprompt", (e) => {
            e.preventDefault();
            deferredPrompt = e;
            document.getElementById("installPrompt").style.display = "block";
        });
        
        document.getElementById("installBtn").addEventListener("click", async () => {
            if (deferredPrompt) {
                deferredPrompt.prompt();
                const { outcome } = await deferredPrompt.userChoice;
                if (outcome === "accepted") {
                    document.getElementById("installPrompt").style.display = "none";
                }
                deferredPrompt = null;
            }
        });
        
        document.getElementById("dismissBtn").addEventListener("click", () => {
            document.getElementById("installPrompt").style.display = "none";
        });
        
        // Вспомогательные функции
        function showMessage(message, type) {
            // Реализация показа сообщений
            console.log(`${type}: ${message}`);
        }
    </script>
</body>
</html>

2. Manifest файл

{
  "name": "Мое PWA приложение",
  "short_name": "Мое PWA",
  "description": "Прогрессивное веб-приложение с отличным UX",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4a90e2",
  "orientation": "portrait-primary",
  "scope": "/",
  "lang": "ru",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/mobile.png",
      "sizes": "375x667",
      "type": "image/png",
      "form_factor": "narrow"
    },
    {
      "src": "/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ],
  "categories": ["business", "productivity"],
  "edge_side_panel": {
    "preferred_width": 400
  }
}

3. Service Worker для PWA

// sw.js
const CACHE_NAME = "my-pwa-cache-v1";
const urlsToCache = [
  "/",
  "/index.html",
  "/styles/main.css",
  "/scripts/app.js",
  "/images/logo.png",
  "/manifest.json"
];

// Установка Service Worker
self.addEventListener("install", event => {
  console.log("Service Worker: Установка");
  
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log("Service Worker: Кэширование файлов");
        return cache.addAll(urlsToCache);
      })
      .then(() => self.skipWaiting())
  );
});

// Активация Service Worker
self.addEventListener("activate", event => {
  console.log("Service Worker: Активация");
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache => {
          if (cache !== CACHE_NAME) {
            console.log("Service Worker: Очистка старого кэша", cache);
            return caches.delete(cache);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

// Перехват запросов
self.addEventListener("fetch", event => {
  // Пропускаем не-GET запросы и chrome-extension
  if (event.request.method !== "GET" || event.request.url.startsWith("chrome-extension")) {
    return;
  }
  
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Возвращаем кэшированную версию, если есть
        if (response) {
          return response;
        }
        
        // Клонируем запрос
        const fetchRequest = event.request.clone();
        
        // Выполняем сетевой запрос
        return fetch(fetchRequest).then(response => {
          // Проверяем валидность ответа
          if (!response || response.status !== 200 || response.type !== "basic") {
            return response;
          }
          
          // Клонируем ответ
          const responseToCache = response.clone();
          
          // Кэшируем новый ресурс
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });
          
          return response;
        });
      })
      .catch(() => {
        // Fallback для offline
        if (event.request.destination === "document") {
          return caches.match("/offline.html");
        }
        
        // Fallback для изображений
        if (event.request.destination === "image") {
          return caches.match("/images/placeholder.jpg");
        }
        
        return new Response("Оффлайн контент недоступен", {
          status: 503,
          statusText: "Service Unavailable"
        });
      })
  );
});

// Фоновая синхронизация
self.addEventListener("sync", event => {
  if (event.tag === "background-sync") {
    console.log("Service Worker: Фоновая синхронизация");
    event.waitUntil(doBackgroundSync());
  }
});

async function doBackgroundSync() {
  // Реализация фоновой синхронизации
  const requests = await getPendingRequests();
  for (const request of requests) {
    try {
      await fetch(request.url, {
        method: request.method,
        body: request.body,
        headers: request.headers
      });
      await markRequestAsCompleted(request.id);
    } catch (error) {
      console.error("Ошибка фоновой синхронизации:", error);
    }
  }
}

// Push уведомления
self.addEventListener("push", event => {
  const options = {
    body: event.data ? event.data.text() : "У вас новое уведомление",
    icon: "/icons/icon-192x192.png",
    badge: "/icons/badge-72x72.png",
    vibrate: [200, 100, 200],
    data: {
      url: "/"
    },
    actions: [
      {
        action: "open",
        title: "Открыть приложение"
      },
      {
        action: "close",
        title: "Закрыть"
      }
    ]
  };
  
  event.waitUntil(
    self.registration.showNotification("Мое PWA", options)
  );
});

self.addEventListener("notificationclick", event => {
  event.notification.close();
  
  if (event.action === "open") {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    );
  }
});