Service Worker + Push API + VAPID ключи + Notifications API = полноценные push-уведомления в браузере. Никаких внешних сервисов, полный контроль над функциональностью.

В отличие от нативных приложений, веб-push не требует установки, обновляется автоматически и работает на всех платформах. Единственная сложность — особенности iOS, где Apple добавила поддержку только в 2023 году.

Архитектура push-уведомлений

Система push-уведомлений — это сложная экосистема из четырех компонентов. Они работают вместе, чтобы доставить уведомление от вашего сервера до пользователя. Каждый компонент отвечает за свою часть процесса, но без остальных ничего не работает.

Общий принцип работы

  1. Пользователь подписывается на уведомления через веб-приложение

  2. Браузер создает уникальную подписку с криптографическими ключами

  3. Сервер отправляет зашифрованное сообщение через push-сервис на эту подписку

  4. Push-сервис доставляет сообщение браузеру пользователя

  5. Service Worker получает событие и обрабатывает его

  6. Notifications API отображает уведомление пользователю

Давайте теперь разберем каждый из них и поймем, кто что делает и за что отвечает.

1. Service Worker — Фоновый обработчик

Роль: JavaScript-скрипт, работающий в фоне для обработки push-событий

Функции:

  • Получение push-событий от браузера

  • Обработка входящих сообщений

  • Показ уведомлений через Notifications API

  • Работа даже при закрытом браузере

Технические особенности:

  • Работает в отдельном потоке от основного приложения

  • Не имеет доступа к DOM

  • Полностью асинхронный и неблокирующий

  • Требует HTTPS для работы (кроме localhost)

Service Worker — это тип web worker, который действует как прокси-сервер между веб-приложениями, браузером и сетью. Service Worker регистрируется для определенной области (scope), которая определяется origin (домен + протокол + порт) и путем.

Простыми словами: Service Worker — это JavaScript скрипт, который работает в фоне даже когда пользователь закрыл браузер. Он может перехватывать сетевые запросы, кэшировать файлы и показывать push-уведомления.

Техническая архитектура

Service Worker работает в отдельном потоке от основного JavaScript приложения и не имеет доступа к DOM. Это полностью асинхронный и неблокирующий процесс, который не может использовать синхронные API, такие как XHR или Web Storage.

Безопасность: Service Worker доступен только в secure contexts, то есть по HTTPS. Исключение составляет http://localhost для разработки, но в Firefox можно тестировать по HTTP через настройки DevTools.

Модули: Service Worker не может динамически импортировать JavaScript модули — вызов import() в глобальной области видимости вызовет ошибку. Однако статические импорты через import разрешены.

Жизненный цикл Service Worker

Service Worker проходит через основные этапы: регистрация → установка → активация → ожидание → завершение → перезапуск. Для push-уведомлений важно понимать, что SW работает в фоне и может получать события даже при закрытом браузере.

Обновления Service Worker:

Браузер автоматически проверяет обновления Service Worker. При изменении файла новая версия загружается в фоне и активируется когда все страницы закрыты. Для push-уведомлений это означает, что обновления не прерывают работу уведомлений.

Важные моменты:

  • Service Worker контролирует client (любая открытая страница, URL которой попадает в область действия Service Worker. Конкретно это экземпляры WindowClient), а не страницы напрямую

  • Client может быть контролируемым только одним Service Worker одновременно

  • При обновлении Service Worker старые client продолжают использовать старую версию до закрытия всех вкладок

2. Push API — Система доставки

Роль: Веб-стандарт для асинхронной доставки сообщений от сервера

Функции:

  • Создание подписок на push-уведомления

  • Управление жизненным циклом подписок

  • Шифрование и аутентификация сообщений

  • Интеграция с push-сервисами (FCM, Mozilla)

Технические особенности:

  • Использует протокол Web Push (RFC 8030)

  • Основан на HTTP/2 и эллиптической криптографии P-256

  • Каждая подписка имеет уникальный endpoint URL

  • Работает через push-сервисы браузеров (FCM, Mozilla Push Service)

Push API — это веб-стандарт, который позволяет веб-приложениям получать push-сообщения от сервера, даже когда приложение не активно в браузере или полностью закрыто.

Представьте это как систему доставки писем: ваш сервер отправляет "письмо" через почтовую службу (push-сервис), а браузер получает его и передает Service Worker для обработки. Пользователь получает уведомление, даже если сайт закрыт.

Техническая архитектура

Push API работает через сложную систему взаимодействия между браузером, push-сервисом (FCM, Mozilla Push Service) и вашим сервером. Когда пользователь подписывается на уведомления, браузер создает уникальный endpoint URL, который содержит криптографические ключи для аутентификации и шифрования.

Push API работает асинхронно и неблокирующе — браузер может получать сообщения даже когда все вкладки закрыты, а Service Worker автоматически активируется для обработки входящих событий.

Важные моменты:

  • Push API требует HTTPS для работы (кроме localhost)

  • Каждая подписка имеет уникальный endpoint и ключи аутентификации

  • Push-сервисы (FCM, Mozilla) могут ограничивать частоту отправки сообщений

  • Push API не гарантирует доставку — браузер может отклонить сообщения при нехватке ресурсов

  • Подписки могут автоматически обновляться браузером без уведомления разработчика

3. VAPID ключи — Аутентификация

Роль: Криптографические ключи для идентификации сервера

Функции:

  • Аутентификация сервера при отправке уведомлений

  • Предотвращение спама и злоупотреблений

  • Шифрование сообщений (RFC 8291)

  • Связь с владельцем сервера

Технические особенности:

  • Стандарт RFC 8292 для идентификации сервера

  • Использует эллиптическую криптографию P-256

  • Ключи в формате base64url, минимум 65 байт

  • Обязательны для работы с push-сервисами

VAPID (Voluntary Application Server Identification) — это стандарт RFC 8292, разработанный IETF для идентификации сервера при отправке push-уведомлений.

Представьте VAPID ключи как паспорт для вашего сервера. Без них браузеры просто откажутся принимать ваши уведомления — как пограничник, который не пропускает без документов.

Генерация VAPID ключей

Согласно RFC 8292, VAPID использует криптографию на эллиптических кривых P-256. Ключи должны быть в формате base64url и иметь минимальную длину 65 байт.

Где взять VAPID ключи?

Самый простой и надежный способ — использовать npm пакет web-push с открытым исходным кодом. Эта библиотека предоставляет все необходимое для работы с push-уведомлениями, включая генерацию VAPID ключей.

# Установка библиотеки
npm install -g web-push

# Генерация ключей
web-push generate-vapid-keys

# Или через npx (без установки)
npx web-push generate-vapid-keys

Результат:

=======================================

Public Key:
BO0EswuFP5ApodlzrXx85I4b_uh1C1YQggYv7wggqSksMV9qGOL_A1URE0fQ2J3eH4K0xzOGnXwQiUyXMvrjWGE

Private Key:
xMd5-BGsKWo-H1n__KbNZABGsTF9x9AM8Et1qM84rR8

=======================================

4. Notifications API — Отображение

Роль: Веб-стандарт для показа системных уведомлений

Функции:

  • Запрос разрешений у пользователя

  • Создание и отображение уведомлений

  • Обработка взаимодействий пользователя

  • Интеграция с операционной системой

Технические особенности:

  • Интегрируется с системными API ОС (Windows Action Center, macOS Notification Center)

  • Работает только в secure contexts (HTTPS или localhost)

  • Требует явного разрешения пользователя

  • Поддерживает действия (actions) в ограниченном количестве браузеров

Notifications API — это веб-стандарт, который позволяет веб-страницам управлять отображением системных уведомлений пользователю. Эти уведомления отображаются вне области просмотра браузера, поэтому могут показываться даже когда пользователь переключился на другую вкладку или приложение.

Техническая архитектура

Notifications API работает через тесную интеграцию с операционной системой и браузерным движком. Когда веб-приложение запрашивает разрешение на показ уведомлений, браузер обращается к системным API для создания канала уведомлений, который связывает веб-приложение с нативными возможностями ОС.

С технической точки зрения, Notifications API использует системные механизмы уведомлений каждой платформы: Windows Action Center, macOS Notification Center, Linux desktop notifications через D-Bus. Браузер выступает в роли моста между веб-стандартом и нативными API операционной системы.

Notifications API работает асинхронно и неблокирующе — создание и отображение уведомлений не влияет на производительность основного приложения. Уведомления имеют собственный жизненный цикл и могут взаимодействовать с пользователем через события клика, закрытия или действий.

Важные моменты:

  • Notifications API требует явного разрешения пользователя перед показом уведомлений

  • Разрешение может быть отозвано пользователем в любой момент через настройки браузера

  • Уведомления имеют ограничения по размеру и количеству (зависят от браузера и ОС)

  • Notifications API работает только в secure contexts (HTTPS или localhost)

  • Уведомления могут быть заблокированы системными настройками "Не беспокоить"

  • Каждое уведомление имеет уникальный ID и может быть обновлено или закрыто программно


Настройка PWA для работы push-уведомлений

Давайте настроим PWA чтобы наши push-уведомления могли работать везде, включая iOS.

Web Application Manifest

Web Application Manifest — это JSON файл, который предоставляет информацию о веб-приложении для браузера. Основная цель — превратить обычный сайт в Progressive Web App (PWA).

Создание manifest.json

{
  "short_name": "Push App",
  "name": "Push Notifications App",
  "description": "Приложение с поддержкой push-уведомлений",
  "icons": [
    {
      "src": "/apple-touch-icon.png",
      "sizes": "180x180",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/nextjs-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/nextjs-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/favicon.ico",
      "sizes": "16x16 32x32",
      "type": "image/x-icon",
      "purpose": "any"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#27c9b4",
  "background_color": "#f4f5f7",
  "scope": "/",
  "orientation": "portrait-primary"
}

Подключение в HTML

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Push Notifications</title>
    
    <!-- Web App Manifest -->
    <link rel="manifest" href="/manifest.json">
    
    <!-- Meta теги для PWA -->
    <meta name="theme-color" content="#27c9b4">
    <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="Push App">
    
    <!-- Иконки -->
    <link rel="icon" type="image/png" href="/favicon.ico">
    <link rel="apple-touch-icon" href="/apple-touch-icon.png">
</head>
<body>
    <!-- Ваш контент -->
</body>
</html>

Ключевые параметры для push-уведомлений

display: "standalone" — приложение запускается в полноэкранном режиме без адресной строки браузера

scope: "/" — определяет область действия PWA (должна совпадать с scope Service Worker)

start_url: "/" — URL, который открывается при запуске приложения

icons — иконки для установки на главный экран (обязательно для iOS)


От теории к практике

Переходим к практической реализации push-уведомлений:

  1. Service Worker — обработка входящих push-событий и показ уведомлений

  2. Клиентская часть — регистрация Service Worker, запрос разрешений, подписка на push

  3. Серверная часть — отправка push-уведомлений с использованием VAPID ключей

  4. Особенности iOS — ограничения и способы их обхода

Практический пример

Создадим простой web service для обработки и отображения уведомлений.

Service Worker (sw.js)

self.addEventListener('install', (event) => {
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

self.addEventListener('push', (event) => {
  if (event.data) {
    try {
      const pushData = event.data.json();
      self.registration.showNotification(pushData.title, pushData);
    } catch (error) {
      console.error('Push notification error:', error);
    }
  }
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
    event.waitUntil(
    clients.openWindow('/')
    );
});

Разбор кода

Давайте разберем, что здесь написано:

Что за объект self?

self — это глобальный объект в контексте Service Worker, аналогичный window в обычном веб-приложении. Он представляет сам Service Worker и предоставляет доступ к его API.

self.addEventListener('install', (event) => {
  self.skipWaiting();
});

Этот код регистрирует обработчик события install, которое срабатывает при установке или обновлении Service Worker. Внутри обработчика вызывается self.skipWaiting().

Метод skipWaiting() заставляет новый Service Worker немедленно стать активным, минуя обычную процедуру ожидания. Без этого метода новый Service Worker будет установлен, но не активирован до перезагрузки страницы, что означает, что старый Service Worker продолжит работать и пользователь не получит новые функции.


self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

Этот код регистрирует обработчик события activate, которое срабатывает после установки Service Worker, когда он становится активным. Внутри обработчика вызывается event.waitUntil(self.clients.claim()).

Метод self.clients.claim() берет контроль над всеми открытыми страницами приложения, которые еще не контролируются Service Worker. Без этого метода Service Worker будет активен, но не будет контролировать существующие вкладки — они продолжат работать без Service Worker до следующей перезагрузки.

event.waitUntil() гарантирует, что Service Worker не будет завершен до завершения операции clients.claim(). Это критично для push-уведомлений, так как Service Worker должен контролировать все страницы, чтобы правильно обрабатывать входящие уведомления и показывать их пользователю.

Ключевая особенность: waitUntil() может вызываться несколько раз в рамках одного события, и Service Worker будет ждать завершения всех переданных промисов. В случае с activate это позволяет Service Worker завершить все необходимые операции (например, очистку старых кэшей) перед тем, как начать обрабатывать функциональные события типа fetch и push.


self.addEventListener('push', (event) => {
  if (event.data) {
    try {
      const pushData = event.data.json();

      // Пример с расширенными опциями
      const notificationOptions = {
        body: pushData.body || 'Новое сообщение',
        icon: pushData.icon || '/icon-192x192.png',
        badge: pushData.badge || '/badge-72x72.png',
        image: pushData.image,
        tag: pushData.tag || 'default',
        data: pushData.data,
        requireInteraction: pushData.requireInteraction || false,
        silent: pushData.silent || false,
        vibrate: pushData.vibrate || [200, 100, 200],
        timestamp: Date.now(),
        actions: pushData.actions || []
      };
      
      self.registration.showNotification(pushData.title, notificationOptions);
    } catch (error) {
      console.error('Push notification error:', error);
    }
  }
});

Этот код регистрирует обработчик события push, которое срабатывает при получении push-сообщения от сервера. Внутри обработчика проверяется наличие данных в событии.

Если данные присутствуют, код пытается распарсить их как JSON через event.data.json(). Затем создается объект notificationOptions с расширенными настройками уведомления, включая fallback значения для основных опций. Полученные данные используются для показа уведомления через showNotification()

Метод showNotification() создает системное уведомление с заголовком и опциями, которые могут включать текст сообщения, иконку, действия, вибрацию и другие параметры.

Гибкость настройки: showNotification() поддерживает множество опций для кастомизации уведомлений:

  • body — текст сообщения под заголовком

  • icon — URL иконки уведомления

  • image — URL изображения в уведомлении

  • badge — иконка для панели уведомлений (Android)

  • tag — идентификатор для группировки уведомлений

  • data — произвольные данные для передачи в обработчик клика

  • actions — массив кнопок действий (экспериментально)

  • requireInteraction — уведомление не закрывается автоматически

  • silent — отключение звука и вибрации

  • vibrate — паттерн вибрации

  • timestamp — время создания уведомления

  • dir — направление текста (ltr/rtl)

  • lang — язык уведомления

  • renotify — повторное уведомление при замене

Ключевая особенность: showNotification() возвращает промис, который разрешается в undefined при успешном показе уведомления. Если Service Worker не активен или пользователь запретил уведомления, метод выбросит исключение TypeError, поэтому важно оборачивать его в try/catch.


self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  //можно регулировать ссылку вручную или положится на ответ сервера
  const url = event.notification.data?.url || '/';

  event.waitUntil(
    clients.openWindow(url)
  );
});

Этот код регистрирует обработчик события notificationclick, которое срабатывает при клике пользователя по push-уведомлению. Внутри обработчика вызывается event.notification.close() для закрытия уведомления.

Метод close() удаляет уведомление с экрана и из системного трея уведомлений. Это важно делать сразу после клика, чтобы уведомление не оставалось видимым после того, как пользователь уже взаимодействовал с ним.

Затем код извлекает URL из данных уведомления (event.notification.data?.url) или использует дефолтный путь '/'. Это позволяет отправлять персонализированные ссылки в push-уведомлениях. Метод openWindow() открывает новую вкладку с указанным URL.

Про event.waitUntil() мы уже знаем — он гарантирует, что Service Worker не будет завершен до завершения операции.


Клиентская часть

Теперь создадим клиентскую часть для подписки на push-уведомления. Этот код будет работать в браузере и управлять процессом получения разрешений от пользователя.

client.js

const vapidPublicKey = `BO0EswuFP5ApodlzrXx85I4b_uh1C1YQggYv7wggqSksMV9qGOL_A1URE0fQ2J3eH4K0xzOGnXwQiUyXMvrjWGE`;

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

function isPushManagerActive(pushManager) {
  if (!pushManager) {
    console.warn('PushManager is not available');
    return false;
  }
  return true;
}

async function initServiceWorker() {
  try {
    const swRegistration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
    });

    const pushManager = swRegistration.pushManager;

    if (!isPushManagerActive(pushManager)) {
      return;
    }

    const permissionState = await pushManager.permissionState({ userVisibleOnly: true });

    switch (permissionState) {
      case 'prompt': // Разрешение на push-уведомления пока не дано
        return 'prompt';
      case 'granted': { // Разрешение на push-уведомления дано
        const existingSubscription = await pushManager.getSubscription();
        if (existingSubscription) {
          await sendSubscriptionToServer(existingSubscription);
        }
        return 'granted';
      }
      case 'denied': // Пользователь отказал в разрешении push-уведомлений
        return 'denied';
    }
  } catch (error) {
    console.error('Service Worker initialization error:', error);
    return 'error';
  }
}

async function subscribeToPush() {
  if (!vapidPublicKey) {
    console.error('VAPID key is not configured');
    return;
  }

  try {
    const swRegistration = await navigator.serviceWorker.ready;
    const pushManager = swRegistration.pushManager;

    if (!isPushManagerActive(pushManager)) {
      return;
    }

    const subscriptionOptions = {
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
    };

    const subscription = await pushManager.subscribe(subscriptionOptions);
    
    await sendSubscriptionToServer(subscription);
    return true;
  } catch (error) {
    console.error('Push subscription error:', error);
    return false;
  }
}

async function sendSubscriptionToServer(subscription) {
  try {
    const subscriptionJson = subscription.toJSON();

    if (!subscriptionJson.keys?.p256dh || !subscriptionJson.keys?.auth || !subscriptionJson.endpoint) {
      throw new Error('Отсутствуют необходимые данные подписки');
    }

    const subscriptionInfo = {
      endpoint: subscription.endpoint,
      keys: {
        p256dh: subscriptionJson.keys.p256dh,
        auth: subscriptionJson.keys.auth,
      },
    };

    const response = await fetch('/web-push', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(subscriptionInfo),
    });
    
  } catch (error) {
    console.error('Server subscription error:', error);
    throw error;
  }
}



// Запуск при загрузке страницы
if (navigator.serviceWorker && 'PushManager' in window) {
  initServiceWorker().then((state) => {
    if (state === 'prompt') {
      // Показываем UI компонент для запроса разрешения на уведомления который при нажатии будет вызывать нашу функцию `subscribeToPush()`
    }
  });
} else {
  console.warn('Push-уведомления не поддерживаются в этом браузере');
}

Разбор клиентского кода

Основные функции

urlBase64ToUint8Array() — конвертирует VAPID ключ из base64 в Uint8Array формат, который требует Push API.

isPushManagerActive() — проверяет доступность PushManager в Service Worker. Возвращает true если доступен, false если нет.

initServiceWorker() — регистрирует Service Worker и проверяет состояние разрешений. Возвращает: 'prompt', 'granted', 'denied' или 'error'.

subscribeToPush() — создает подписку на push-уведомления с использованием VAPID ключа.

sendSubscriptionToServer() — отправляет данные подписки на сервер для сохранения.

Ключевые методы

navigator.serviceWorker.register('/sw.js', { scope: '/' }) — регистрирует Service Worker ДЛЯ ВСЕГО САЙТА.

Важно: Service Worker можно регистрировать не только для всего сайта, но и для конкретных путей:

// Для всего сайта
navigator.serviceWorker.register('/sw.js', { scope: '/' });

// Для конкретной папки
navigator.serviceWorker.register('/sw.js', { scope: '/admin/' });

// Для конкретной страницы
navigator.serviceWorker.register('/sw.js', { scope: '/dashboard' });

// Без scope (по умолчанию - папка где лежит sw.js)
navigator.serviceWorker.register('/admin/sw.js'); // scope будет '/admin/'

pushManager.permissionState({ userVisibleOnly: true }) — проверяет состояние разрешений на уведомления.

pushManager.subscribe(subscriptionOptions) — создает подписку. Требует пользовательского действия (клик). (Можно и без пользовательского действия, но будет работать не во всех браузерах)

navigator.serviceWorker.ready — возвращает Promise, который разрешается когда Service Worker активен. Promise никогда не отклоняется, только ждет активации. Обязательно использовать перед pushManager.subscribe().

Структура подписки

После успешной подписки pushManager.subscribe() возвращает объект с разными endpoint'ами в зависимости от браузера:

Desktop (Firefox):

{
  endpoint: "https://updates.push.services.mozilla.com/wpush/v2/...",
  keys: {
    p256dh: "BEl62iUYgUivxIkv69yViEuiBIa40HI...",
    auth: "tBHItJI5svbpez7KI4CCXg=="
  },
  expirationTime: 1640995200000
}

Mobile (Android Chrome):

{
  endpoint: "https://fcm.googleapis.com/fcm/send/...",
  keys: {
    p256dh: "BEl62iUYgUivxIkv69yViEuiBIa40HI...",
    auth: "tBHItJI5svbpez7KI4CCXg=="
  },
  expirationTime: null
}

Mobile (iOS Safari 16.4+):

{
  endpoint: "https://web.push.apple.com/...",
  keys: {
    p256dh: "BEl62iUYgUivxIkv69yViEuiBIa40HI...",
    auth: "tBHItJI5svbpez7KI4CCXg=="
  },
  expirationTime: null
}

Desktop (Microsoft Edge):

{
  endpoint: "https://wns2-wns2p.notify.windows.com/...",
  keys: {
    p256dh: "BEl62iUYgUivxIkv69yViEuiBIa40HI...",
    auth: "tBHItJI5svbpez7KI4CCXg=="
  },
  expirationTime: null
}

Минимальный пример backend приложения на express

const express = require('express');
const webpush = require('web-push');

const app = express();
app.use(express.json());

// VAPID ключи (используйте свои)
const VAPID_PUBLIC_KEY = "BO0EswuFP5ApodlzrXx85I4b_uh1C1YQggYv7wggqSksMV9qGOL_A1URE0fQ2J3eH4K0xzOGnXwQiUyXMvrjWGE";
const VAPID_PRIVATE_KEY = "xMd5-BGsKWo-H1n__KbNZABGsTF9x9AM8Et1qM84rR8";

// Настройка VAPID
webpush.setVapidDetails(
    'mailto:your-email@example.com',
    VAPID_PUBLIC_KEY,
    VAPID_PRIVATE_KEY
);

// Пример хранилище подписок
const subscriptions = [];

// Сохранение подписки
app.post('/web-push', (req, res) => {
    const subscription = req.body;
    subscriptions.push(subscription);
    res.status(201).json({ success: true });
});

// Функция отправки уведомления
function sendNotification(title, body, icon) {
    const payload = JSON.stringify({
        title: title || 'Новое уведомление',
        body: body || 'У вас новое сообщение',
        icon: icon || '/icon.png',
        data: { url: '/' }
    });

    return webpush.sendNotification(subscriptions[0], payload);
}

// Пример использования
sendNotification('Привет!', 'Это тестовое уведомление', '/icon.png')
    .then(() => console.log('Notification sent'))
    .catch(err => console.error('Error:', err));

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Готово! Push-уведомления работают везде ?

Теперь у на�� есть полноценная система push-уведомлений, которая работает на всех устройствах и браузерах — даже на iOS!

Как так получилось? Еще недавно iOS был главной головной болью разработчиков, но Apple все-таки сдалась. Давайте посмотрим, что изменилось.

История Service Worker на iOS

Хронология поддержки

iOS 11.3 (март 2018) — Apple добавила базовую поддержку Service Worker, но с серьезными ограничениями:

  • Service Worker работал только в Safari

  • Не поддерживал push-уведомления

  • Ограниченная функциональность

iOS 16.4 (март 2023) — революционное обновление:

  • Полная поддержка push-уведомлений в веб-приложениях

  • Работа через Apple Push Notification service (APNs)

  • Поддержка VAPID ключей

Условия для работы на iOS

1. Версия iOS 16.4+

  • Минимальная версия для push-уведомлений

  • Старые версии iOS полностью не поддерживают web push

2. PWA (Progressive Web App)

  • Приложение должно быть добавлено на главный экран

  • Обычные вкладки Safari не поддерживают push-уведомления

  • Только standalone режим (вне браузера)

3. HTTPS обязательно

  • Работает только в secure contexts

  • HTTP не поддерживается (кроме localhost для разработки)

4. Пользовательское разрешение

  • Обязательное разрешение пользователя

  • Нельзя отправлять уведомления без согласия

Детекция iOS и предложение установки PWA

Хотя мы настроили PWA и push-уведомления теперь работают на iOS, пользователи могут не знать, что нужно установить приложение на главный экран. Эта функция поможет нам определить, распространяются ли ограничения iOS на устройство пользователя, и подсказать правильные действия.

function checkIOSAndPWA() {
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
  const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
                window.navigator.standalone === true;
  
  if (isIOS && !isPWA) {
    console.log("для работы push уведомлений установите приложение на рабочий стол")
  }
}

Заключение

Мы разобрали полную реализацию push-уведомлений в PWA без использования сторонних сервисов. Теперь у вас есть:

Полное понимание архитектуры — Service Worker, Push API, VAPID ключи и Notifications API
Готовый код — от регистрации Service Worker до отправки уведомлений с сервера
PWA настройка — manifest.json и HTML для работы на всех платформах
iOS поддержка — понимание ограничений и способов их обхода

Главные преимущества такого подхода:

  • Полный контроль над функциональностью

  • Никаких внешних зависимостей

  • Работает на всех современных платформах

  • Бесплатно и без ограничений

Теперь вы можете реализовать push-уведомления в любом веб-приложении, превратив его в полноценную PWA с нативными возможностями уведомлений!

Комментарии (2)


  1. Neoldian
    11.09.2025 07:24

    Спасибо за разбор, тоже недавно в это всё погружался. В заголовке "без сторонних сервисов" немного сбивает т.к. все равно присутствует зависимость от FCM, Mozilla Push Service. Полностью независимые пуши это что то типа ntfy.


  1. gmtd
    11.09.2025 07:24

    Хорошо получилось
    Что за нейронка?