Skip to content
IRC-CodingIRC-Coding
pwaservice-workerresponsive-web-appsofflineweb-manifestrazrabotka-prilozhenii

Разработка приложений: PWA и Service Worker

Progressive Web Apps с Service Worker, офлайн-режимом и адаптивным дизайном. Практическое руководство с примерами кода.

I

IRC-Coding Team

11 min read
Разработка приложений: PWA и Service Worker

Разработка приложений: адаптивные веб-приложения, 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');
          }
        });
      })
  );
});

Что происходит:

  1. Install: при первом посещении все статические ресурсы (HTML, CSS, JS, значки) сохраняются в кеш. skipWaiting() делает Service Worker активным сразу.
  2. Activate: когда развёртывается новая версия Service Worker (CACHE_NAME = 'my-pwa-v2'), новый сервис-воркер удаляет все старые кеши.
  3. 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 (например, v1v2), 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.

Назад к блогу
Share:

Nächster Artikel in Разработка программного обеспечения

Weiterlesen
Автоматизация и программирование: скрипты, Cronjob, логирование

Похожие статьи