Установленное PWA умеет ровно то, что и нативное приложение, в плане интеграции с системой: показывать уведомления, ставить значок с числом на иконку, появляться в системном меню «Поделиться». Это и отличает PWA от обычной вкладки в браузере. В этой главе разберём самое практичное — push-уведомления, и коротко пробежимся по соседним возможностям.

Push-уведомления: два API в связке

Push-уведомление в браузере — это две разные технологии, работающие вместе:

  • Push API — механизм, через который сервер просит браузер «разбудить» service worker и передать ему сообщение. Работает даже когда вкладка с сайтом закрыта.
  • Notification API — способ показать системное уведомление с заголовком, текстом и иконкой. Работает как из обычной страницы, так и из service worker.

В классическом сценарии оба API работают связкой: сервер шлёт push → service worker получает его в обработчике push → вызывает Notification API, чтобы показать уведомление пользователю.

Шаг 1. Спросить разрешение

Без явного согласия пользователя браузер не покажет ни одного уведомления. Запросить можно на странице:

async function askForPermission() {
  const result = await Notification.requestPermission();
  if (result === "granted") {
    console.log("Пользователь разрешил уведомления");
  } else {
    console.log("Пользователь отказался или закрыл диалог");
  }
}

Важно: не вызывайте этот метод сразу при загрузке страницы. Браузеры (Chrome, Firefox, Safari) понижают рейтинг сайта, который ломится за разрешением до того, как пользователь успел что-то сделать. Правильный паттерн — вызывать requestPermission в ответ на явное действие: клик по кнопке «Подписаться на обновления».

Шаг 2. Подписаться на push

После того как пользователь разрешил уведомления, нужно получить от браузера subscription — уникальный токен, по которому сервер сможет адресовать push именно этому браузеру:

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
  });
  // Отправить subscription на свой сервер для хранения
  await fetch("/api/save-push-subscription", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription)
  });
}

Если незнаком запрос через fetch с разными HTTP-методами и заголовками — разбор в отдельной статье про Fetch API.

Параметр applicationServerKey — это публичный VAPID-ключ, который сервер тоже знает (приватную часть). Через эту пару ключей сервер потом подписывает push-сообщения, чтобы браузер мог убедиться, что push пришёл именно от того сайта, на который пользователь подписался.

userVisibleOnly: true — обязательный флаг. Он значит «каждый push будет сопровождаться видимым уведомлением». Без него браузеры не разрешают подписку, потому что иначе сервер мог бы тихо запускать service worker без ведома пользователя.

Шаг 3. Принять push в service worker

Когда сервер пришлёт push, браузер запустит service worker и вызовет в нём событие push:

self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? {};
  const title = data.title || "Новое уведомление";
  const options = {
    body: data.body || "",
    icon: "/icons/icon-192.png",
    badge: "/icons/badge.png",
    data: { url: data.url }
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

А чтобы клик по уведомлению открывал нужную страницу:

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  const url = event.notification.data?.url || "/";
  event.waitUntil(self.clients.openWindow(url));
});

Если в момент клика у PWA уже открыто окно — правильнее не открывать новое, а сфокусировать существующее. Полная реализация перебирает clients.matchAll(), ищет открытый клиент с тем же URL и вызывает client.focus().

Шаг 4. Отправить push с сервера

На сервере используется библиотека web-push (Node.js) или её аналог на другом стеке. Минимальный пример на Node:

const webpush = require("web-push");

webpush.setVapidDetails(
  "mailto:admin@example.com",
  VAPID_PUBLIC_KEY,
  VAPID_PRIVATE_KEY
);

await webpush.sendNotification(savedSubscription, JSON.stringify({
  title: "Заказ готов",
  body: "Ваш кофе можно забирать",
  url: "/orders/12345"
}));

Браузер пользователя получает push через свой push-сервис (FCM у Google, Mozilla Push Service у Firefox) и пробуждает service worker.

Где это работает, где — нет

На десктопе Push API поддерживается Chrome, Firefox и Edge. В Safari на macOS работает, но через собственный механизм Apple Push Notification Service и исторически требовал отдельной регистрации. В Safari на iOS push в PWA появился только с версии iOS 16.4 и работает только в установленном на главный экран PWA — в обычной мобильной вкладке Safari push в PWA-сценарии не поддерживается.

Это критично знать заранее: если ваша аудитория — в основном iPhone-пользователи, рассчитывать только на push-уведомления как канал коммуникации нельзя. Нужен резервный канал (email, SMS, сообщения внутри приложения).

Соседние возможности: badging, share target

Кроме push, PWA умеет ещё пару системных интеграций.

Badging. Число на иконке приложения, как у почты или мессенджера. Управляется из обычной страницы:

// Установить значок
if ("setAppBadge" in navigator) {
  navigator.setAppBadge(unreadCount);
}

// Убрать значок
if ("clearAppBadge" in navigator) {
  navigator.clearAppBadge();
}

Поддерживается в Chrome, Edge и Safari на macOS. В Safari на iOS не работает.

Share Target. PWA можно добавить в системное меню «Поделиться» — чтобы пользователь мог отправить картинку или текст из другого приложения прямо в ваш PWA. Указывается в манифесте:

"share_target": {
  "action": "/share",
  "method": "POST",
  "enctype": "multipart/form-data",
  "params": {
    "title": "title",
    "text": "text",
    "url": "url",
    "files": [
      { "name": "photo", "accept": ["image/*"] }
    ]
  }
}

После установки PWA становится приёмником в системном меню «Поделиться»: пользователь делает скриншот, тапает «Поделиться», в списке появляется иконка вашего приложения, и при выборе на ваш /share приходит POST с файлом.

Поддерживается в Chrome и Edge на Android и Windows. На iOS, как и многое другое в PWA, не работает.