Разработка приложений: адаптивные веб-приложения, PWA и Service Worker
Progressive Web Apps (PWA) объединяют лучшее от веб- и нативных приложений: работают в браузере, могут быть установлены, функционируют без сети и отправляют push-уведомления. Они дают охват веба с пользовательским опытом нативного приложения — без процесса одобрения в app store, без 30% комиссии, без специфичного для платформы кода.
В этом руководстве ты научишься создавать PWA: от манифеста через Service Worker до оффлайн-данных и push-уведомлений.
TL;DR — PWA за 90 секунд
PWA это веб-приложение, которое можно установить, работает без сети и ведёт себя как нативное приложение.
---
Три опоры: Web App Manifest (возможность установки), Service Worker (оффлайн и кеширование), адаптивный дизайн (все устройства).
Service Worker: JavaScript работает в фоне, перехватывает сетевые запросы и отвечает из кеша.
Стратегии кеширования: Cache-First (статика), Network-First (динамика), Stale-While-Revalidate (оба подхода).
Конец краткого объяснения!
Что такое Progressive Web App?
PWA это веб-приложение, которое:
- можно установить на рабочий стол и мобильные устройства, появляется в меню приложений и на главном экране
- работает без сети с помощью Service Worker, кеширующего ресурсы и данные
- может получать push-уведомления даже когда приложение закрыто
- адаптивно подстраивается под любое устройство (мобильное, планшет, настольное)
- безопасно требуется HTTPS для Service Worker
- предоставляет опыт приложения полноэкранный режим, splash-screen, без браузерного интерфейса
PWA vs. нативное приложение vs. веб-приложение
| Функция | PWA | Нативное приложение | Веб-приложение |
|---|---|---|---|
| Установка | Промпт в браузере | App Store | Не устанавливается |
| Работа без сети | ✅ (Service Worker) | ✅ | ❌ |
| Push-уведомления | ✅ | ✅ | ❌ |
| Доступ к железу | Ограничен | Полный | Очень ограничен |
| App Store | Опционально | Требуется | Не нужен |
| Разработка | Одна кодовая база | Для каждой платформы | Одна кодовая база |
| Обновление | Автоматически (браузер) | Проверка App Store | Автоматически |
| Стоимость | Бесплатно | 99$/год (Apple), 25$ (Google) | Бесплатно |
Три опоры PWA
1. Web App Manifest
Манифест это JSON-файл, определяющий как приложение устанавливается и выглядит. Без манифеста установка невозможна.
{
"name": "Мой PWA",
"short_name": "PWA",
"description": "Прогрессивное веб-приложение с оффлайн-функциональностью",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Новая заметка",
"url": "/new",
"icons": [{ "src": "/icons/shortcut.png", "sizes": "96x96" }]
}
]
}
Объяснение ключевых полей:
display: "standalone"приложение запускается без браузерного интерфейса (адресная строка, навигация)theme_colorцвет строки статуса на мобильных устройствахpurpose: "maskable"значок можно обрезать под разные формы (Android)shortcutsбыстрые действия в меню приложения (правый клик на рабочем столе, долгое нажатие на мобильном)
Подключение в HTML:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<!-- iOS-специфичные: -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
Зачем iOS-специфичные мета-теги? Safari частично игнорирует манифест. Теги apple-* обеспечивают корректную установку и отображение приложения на iOS.
2. Service Worker — Сердце системы
Service Worker это JavaScript-воркер, работающий в фоне независимо от веб-страницы. Он выступает посредником между приложением и сетью: каждый запрос проходит через Service Worker, который решает ответить из кеша или загрузить с сетевого сервера.
Жизненный цикл Service Worker:
Install → Activate → Fetch/Message Events (постоянно)
↓ ↓
Кеширование Удаление старых кешей
// sw.js — Service Worker
const CACHE_NAME = 'my-pwa-v1';
const ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icons/icon-192.png',
'/offline.html' // Страница fallback
];
// УСТАНОВКА: предварительное кеширование ресурсов
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting()) // Активировать сразу, не ждать
);
});
// АКТИВАЦИЯ: удаление старых кешей (версионирование)
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_NAME) // Все кроме текущего кеша
.map(key => caches.delete(key)) // Удалить
);
}).then(() => self.clients.claim()) // Сразу взять контроль
);
});
// ЗАПРОСЫ: стратегия Cache-First (для статических ресурсов)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response; // Из кеша
}
// Не в кеше → загрузить с сети
return fetch(event.request).catch(() => {
// Сеть недоступна → fallback оффлайн
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
Что происходит:
- Install: при первом посещении все статические ресурсы (HTML, CSS, JS, значки) сохраняются в кеш.
skipWaiting()делает Service Worker активным сразу. - Activate: когда развёртывается новая версия Service Worker (
CACHE_NAME = 'my-pwa-v2'), новый сервис-воркер удаляет все старые кеши. - Fetch: каждый запрос сначала ищется в кеше. Найден → ответить из кеша (быстро, работает оффлайн). Не найден → загрузить с сети. Сеть недоступна → показать страницу оффлайна.
3. Адаптивный дизайн
Адаптивный дизайн не является PWA-специфичной технологией, но критически важен приложение должно хорошо выглядеть на мобильных, планшетах и настольных компьютерах.
/* Mobile-First: базовые стили для мобильных */
.container {
width: 100%;
padding: 1rem;
font-size: 16px;
}
/* Навигация: hamburger на мобильном, горизонтальная на рабочем столе */
.nav {
display: flex;
flex-direction: column; /* Мобильный: друг под другом */
}
/* Планшет (768px+) */
@media (min-width: 768px) {
.container {
max-width: 720px;
margin: 0 auto;
}
.nav {
flex-direction: row; /* Планшет: рядом */
}
}
/* Рабочий стол (1024px+) */
@media (min-width: 1024px) {
.container {
max-width: 960px;
}
}
/* Большой рабочий стол (1440px+) */
@media (min-width: 1440px) {
.container {
max-width: 1200px;
}
}
Принцип Mobile-First: начни со стилей для мобильных (самые маленькие экраны) и затем добавляй media queries с min-width для больших экранов. Это эффективнее Desktop-First подхода с max-width, потому что мобильные устройства обрабатывают меньше CSS.
Регистрация Service Worker
Service Worker нужно зарегистрировать из основного приложения:
// app.js
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) {
// Доступна новая версия — уведомить пользователя
showUpdateNotification();
}
});
});
})
.catch(error => {
console.error('Ошибка регистрации Service Worker:', error);
});
});
}
// Применить обновление
function applyUpdate() {
navigator.serviceWorker.getRegistration().then(reg => {
if (reg.waiting) {
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
}
});
window.location.reload();
}
Важно:
- Service Worker должны доставляться через HTTPS (исключение:
localhostдля разработки) - Путь к Service Worker определяет его область видимости.
/sw.jsконтролирует всю домен - Service Worker устанавливаются при первом посещении и загружаются из кэша при последующих посещениях
Стратегии кэширования: когда что использовать
Выбор правильной стратегии кэширования критичен для производительности и работоспособности приложения в офлайне.
Cache-First (для статических ресурсов)
Всегда отвечает из кэша, если он доступен. Обращается в сеть только если кэша нет.
// Идеально для: CSS, JS, шрифтов, иконок — файлы которые редко меняются
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
Когда использовать? Статические ресурсы, которые не меняются между развертываниями. При новом развертывании меняется имя кэша и старые кэши удаляются.
Network-First (для динамического контента)
Сначала пытается получить данные из сети. Если это не удается (офлайн), возвращает данные из кэша.
// Идеально для: API-запросов, новостных лент, данных пользователя
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(response => {
// Сохранить успешный ответ в кэш
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => {
// Офлайн → возвращаем из кэша
return caches.match(event.request);
})
);
});
Когда использовать? Динамический контент, который должен быть всегда актуален, но при этом доступен в офлайне.
Stale-While-Revalidate (лучшее из обоих миров)
Сразу отвечает из кэша (быстро) и одновременно получает новую версию в фоне (актуально).
// Идеально для: изображений, шрифтов, некритичных ресурсов
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
}).catch(() => cached);
return cached || fetchPromise;
})
);
});
Когда использовать? Ресурсы, которые иногда обновляются, но немного устаревшая версия приемлема. Пользователь получает ответ сразу, а при следующем посещении будет свежая версия.
Сравнение стратегий
| Стратегия | Скорость | Актуальность | Офлайн | Применение |
|---|---|---|---|---|
| Cache-First | Очень быстро | Устаревает до обновления | ✅ | Статические ресурсы |
| Network-First | Медленно (сеть) | Всегда актуально | ✅ (старый кэш) | Динамический контент |
| Stale-While-Revalidate | Очень быстро | Немного устаревает | ✅ | Изображения, шрифты |
Комбинированный подход
На практике используются разные стратегии для разных типов запросов:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API-запросы: Network-First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Изображения: Stale-While-Revalidate
if (event.request.destination === 'image') {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Всё остальное: Cache-First
event.respondWith(cacheFirst(event.request));
});
Push-уведомления
Push-уведомления позволяют приложению доставлять сообщения пользователям даже когда оно закрыто. Это требует сервис отправки пушей (например, Firebase Cloud Messaging или Web Push).
// 1. Запросить разрешение
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('Пользователь отклонил уведомления');
return;
}
// 2. Создать подписку на пуши
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Уведомления должны быть видны
applicationServerKey: VAPID_PUBLIC_KEY // Открытый VAPID-ключ
});
// 3. Отправить подписку на сервер
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
console.log('Подписка на пуши зарегистрирована');
}
// 4. Получить пуш в Service Worker
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Открыть' },
{ action: 'close', title: 'Закрыть' }
]
})
);
});
// 5. Обработать клик на уведомление
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Что нужно:
- VAPID-ключи (генерируются с помощью
npx web-push generate-vapid-keys) - Сервер для отправки push-уведомлений (например, используя npm пакет
web-push) - HTTPS (требуется для Service Worker)
Ограничение iOS: Push-уведомления на iOS работают только с iOS 16.4 и выше, и только если приложение добавлено на экран Home.
Офлайн-данные с IndexedDB
Cache API хорошо подходит для статических ресурсов, но не для структурированных данных. Для офлайн-данных (например, задач, заметок, данных пользователя) используется IndexedDB.
// IndexedDB асинхронна и основана на callback-ах, но idb делает её Promise-based
// npm install idb
import { openDB } from 'idb';
const db = await openDB('my-pwa-db', 1, {
upgrade(db) {
// Создать хранилище объектов (как таблица)
const store = db.createObjectStore('todos', { keyPath: 'id' });
// Индекс для более быстрого поиска
store.createIndex('by-status', 'status');
store.createIndex('by-date', 'createdAt');
}
});
// Сохранить данные (в офлайне)
async function saveTodo(todo) {
await db.put('todos', {
id: crypto.randomUUID(),
title: todo.title,
status: 'pending',
createdAt: Date.now()
});
}
// Прочитать данные
async function getTodos() {
return await db.getAll('todos');
}
// Фильтровать по статусу
async function getPendingTodos() {
return await db.getAllFromIndex('todos', 'by-status', 'pending');
}
// Удалить данные
async function deleteTodo(id) {
await db.delete('todos', id);
}
Cache API vs. IndexedDB:
- Cache API: Для HTTP-ответов (HTML, CSS, JS, изображения). Ключ = запрос, значение = ответ.
- IndexedDB: Для структурированных данных (JSON-объекты, данные пользователя, офлайн-изменения). Ключ = любой ключ, значение = любой объект.
Background Sync (синхронизация офлайн-изменений)
Когда пользователь вносит изменения в офлайн-режиме, Background Sync может синхронизировать их автоматически, как только восстанавливается соединение:
// В приложении: регистрируем синхронизацию
async function syncTodos() {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-todos');
}
// В Service Worker: обрабатываем sync-событие
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-todos') {
event.waitUntil(syncTodosWithServer());
}
});
async function syncTodosWithServer() {
const pendingTodos = await db.getAllFromIndex('todos', 'by-status', 'pending');
for (const todo of pendingTodos) {
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo)
});
await db.put('todos', { ...todo, status: 'synced' });
}
}
PWA с фреймворками
Vite PWA Plugin (рекомендуется для Vite-проектов)
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate', // Автоматические обновления
manifest: {
name: 'Мое приложение',
short_name: 'PWA',
theme_color: '#000000',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 3600 }
}
}
]
}
})
]
};
Преимущество: Vite PWA автоматически генерирует Service Worker с помощью Workbox. Вам не нужно писать Service Worker вручную.
Next.js PWA
npx create-next-app@latest my-pwa --typescript
npm install @ducanh2912/next-pwa
// next.config.js
const withPWA = require('@ducanh2912/next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
});
module.exports = withPWA({});
Astro PWA
Astro предоставляет официальную интеграцию PWA:
npx astro add astro-pwa
Тестирование PWA
Lighthouse Audit
Lighthouse проверяет соответствие PWA и выставляет оценку:
npx lighthouse https://localhost:3000 --view --preset=pwa
Lighthouse проверяет:
- Наличие и корректность манифеста
- Регистрацию Service Worker
- Активность HTTPS
- Адаптивность на мобильных устройствах
- Офлайн-функциональность
- Возможность установки
Отладка Service Worker
Chrome DevTools → Application → Service Workers:
- отображение статуса (active, waiting, installed)
- опция “Update on reload” для разработки
- “Bypass for network” для отключения Service Worker
- “Unregister” для удаления Service Worker
Ручное тестирование офлайн-режима
Chrome DevTools → Application → Service Workers → включите флажок “Offline”. Затем перезагрузите страницу и проверьте, работает ли офлайн-режим.
Обнаружение установки приложения
Вы можете перехватить промпт установки и показать собственную кнопку “Установить”:
let deferredPrompt;
// Браузер выводит beforeinstallprompt (перед автоматическим промптом)
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Предотвращаем автоматический промпт
deferredPrompt = e;
showInstallButton(); // Показываем свою кнопку
});
// Пользователь кликает на кнопку установки
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt(); // Выводим диалог установки
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('Пользователь установил приложение');
hideInstallButton();
} else {
console.log('Пользователь отказался от установки');
}
deferredPrompt = null; // Промпт можно использовать только один раз
});
// Приложение было установлено
window.addEventListener('appinstalled', () => {
console.log('Приложение успешно установлено');
// Отправляем аналитическое событие
});
Важно: на iOS beforeinstallprompt не срабатывает. Пользователи iOS должны вручную выбрать “На экран «Домой»”.
Ключевые моменты
- PWA: Progressive Web App, устанавливается, работает офлайн, адаптивна, защищена HTTPS
- 3 столпа: Web App Manifest (установка), Service Worker (офлайн/кеширование), Responsive Design (все устройства)
- Manifest: JSON-файл с name, icons, display, theme_color, start_url
- Жизненный цикл Service Worker: Install (предварительное кеширование) → Activate (удаление старых кешей) → Fetch (перехват запросов)
- Стратегии кеширования: Cache-First (статика), Network-First (динамика), Stale-While-Revalidate (гибридный подход)
- Push-уведомления: VAPID-ключи, Push-subscription, обработчик
pushв Service Worker - IndexedDB: офлайн-база данных для структурированных данных (в отличие от Cache API для HTTP-ответов)
- Background Sync: автоматическая синхронизация офлайн-изменений
- HTTPS: обязателен для Service Worker (исключение: localhost)
- Тестирование: Lighthouse PWA Audit, вкладка Application в Chrome DevTools
FAQ
Заменяют ли PWA нативные приложения? Для многих сценариев да, особенно для контент-приложений, e-commerce и инструментов. Ограничения есть при работе с железом (Bluetooth, NFC, сенсоры), фоновых процессах и присутствии в app-store. Для игр и приложений, близких к железу, нативные приложения остаются лучшим выбором.
Нужен ли фреймворк для PWA? Нет, PWA работают с обычным JavaScript, достаточно манифеста и Service Worker. Фреймворки (Vite PWA, Next-PWA) упрощают настройку и автоматически генерируют Service Worker с помощью Workbox.
Работают ли PWA на iOS?
Да, с ограничениями. Push-уведомления доступны с iOS 16.4. Background Sync не поддерживается. Промпт установки нужно вызвать вручную через “Поделиться → На экран «Домой»” (события beforeinstallprompt нет).
Как обновить мою PWA?
При каждом деплое: 1) загружаем новые ассеты, 2) увеличиваем название кеша в Service Worker (например, v1 → v2), 3) браузер при следующем посещении заметит, что Service Worker изменился, и установит новую версию. С помощью skipWaiting() новая версия активируется немедленно.
В чем разница между Cache API и IndexedDB? Cache API сохраняет HTTP-ответы (HTML, CSS, JS, изображения), где ключ это запрос, значение это ответ. IndexedDB сохраняет структурированные данные (JSON-объекты) с произвольными ключами и индексами. Используйте Cache API для статических ассетов, IndexedDB для данных пользователя и офлайн-изменений.
Можно ли комбинировать PWA и SPA (React/Vue/Svelte)? Да, это даже стандартный подход. SPA работает как веб-приложение, Service Worker кеширует ассеты SPA, а манифест делает её устанавливаемой. Vite PWA Plugin из коробки поддерживает React, Vue, Svelte и другие фреймворки.
Дополнительные материалы
Keine Bücher für Kategorie "web-development" gefunden.


