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)
);
}
});