Web Push API: Полное руководство по реализации push-уведомлений
-
Присаживайте по удобнее, заварите чайку тема будет объемная и большая…
А делиться я буду своим опытом реализации push-уведомлений на веб-приложениях с помощью Web Push API. Это настоящий киллер-фича, который позволяет держать пользователей в курсе даже тогда, когда они не находятся на вашем сайте. Давайте разберемся с этой технологией во всех деталях.
Но давайте с самого начала - Что такое Web Push API и зачем это нужно?
Web Push API - это стандартный API браузера, который позволяет отправлять пользователям нативные push-уведомления, не полагаясь на сторонние сервисы вроде Firebase или Pusher. Звучит просто? На самом деле это мощная технология, которая работает прямо из коробки во всех современных браузерах.
Главные преимущества:
- Пользователи получают уведомления даже когда браузер закрыт (на большинстве платформ)
- Не нужно платить за сторонние сервисы - все работает через браузер
- Полный контроль над данными и шифрованием
- Стандартизированный подход, поддерживаемый всеми современными браузерами
И да на нашем форум тоже есть пуш-уведомления, но мы их пока дорабатываем

Архитектура Web Push API: из чего всё состоит?
Помимо самой практики не менее важно понимать что вы делает и как это работает. Да-да, вайб-кодеры читайте внимательнее и вникайте! Я что просто так собирал этот материал?

Вся система состоит из четырех ключевых компонентов:
1. Service Worker - Фоновый боец приложения
Service Worker - это JavaScript-скрипт, который работает в фоне, отдельно от основной страницы. Именно благодаря ему уведомления доходят до пользователя даже при закрытом браузере.Что делает Service Worker:
- Получает push-события от браузера
- Обрабатывает входящие уведомления
- Отображает уведомления пользователю
- Обрабатывает клики по уведомлениям
2. VAPID ключи - Паспорт вашего сервера
VAPID (Voluntary Application Server Identification) - это криптографическая пара ключей, которая идентифицирует ваш сервер при отправке уведомлений. Думайте о ней как о паспорте: без него push-сервис браузера просто откажется принимать ваши уведомления.Как это работает:
- Публичный ключ передаётся браузеру
- Приватный ключ хранится на вашем сервере и никогда не передаётся
- Сервер подписывает все отправляемые уведомления приватным ключом
- Push-сервис браузера проверяет подпись с помощью публичного ключа
3. Subscription Object - Билет на доставку
После подписки пользователя, вы получаете объект, который содержит:- Endpoint - уникальный URL для отправки сообщений
- Ключи шифрования (p256dh и auth) - для безопасности данных
- Это всё хранится в вашей базе данных и используется для отправки уведомлений
4. Push Service - Почтальон браузера
Это сервис, управляемый браузером (Google FCM для Chrome, Mozilla для Firefox и т.д.), который:- Получает шифрованные сообщения от вашего сервера
- Хранит их, если пользователь оффлайн
- Доставляет их браузеру пользователя
- Гарантирует доставку даже при нестабильном соединении
Пошаговая реализация
Хватит теории, давайте перейдем к практике, подготовьте свои редакторы, проверьте работает ли нода и начинаем ковыряться.
Шаг 1: Генерируем VAPID ключи
Первое, что нужно сделать - это сгенерировать пару VAPID ключей. Это одноразовая операция для вашего приложения.
Установим web-push:
npm install web-pushГенерируем ключи:
npx web-push generate-vapid-keysВывод будет примерно таким:
Public Key: BKV3l...qA Private Key: 1Ik0...xAСохраните эти ключи в переменных окружения! Например, в
.env:VAPID_PUBLIC_KEY=BKV3l...qA VAPID_PRIVATE_KEY=1Ik0...xA
Шаг 2: Регистрируем Service Worker
Теперь создаем файл Service Worker и регистрируем его на клиенте.
Файл
public/sw.js(Service Worker):// Прослушиваем входящие push-события self.addEventListener('push', event => { // Если сообщение содержит данные, парсим их const data = event.data?.json() || {}; // Подготавливаем параметры уведомления const options = { body: data.body || 'Новое уведомление', icon: data.icon || '/icon-192.png', badge: '/badge.png', vibrate: [200, 100, 200], // Вибрация на мобильных tag: data.tag || 'notification', // Теги для группировки data: { url: data.url || '/' // URL для открытия по клику } }; // Отображаем уведомление event.waitUntil( self.registration.showNotification( data.title || 'Уведомление', options ) ); }); // Обрабатываем клик по уведомлению self.addEventListener('notificationclick', event => { // Закрываем уведомление event.notification.close(); // Открываем URL в окне браузера event.waitUntil( clients.matchAll({ type: 'window' }) .then(clientList => { // Проверяем, не открыт ли уже этот URL for (const client of clientList) { if (client.url === event.notification.data?.url && 'focus' in client) { return client.focus(); } } // Если не открыт, открываем новое окно if (clients.openWindow) { return clients.openWindow(event.notification.data?.url || '/'); } }) ); }); // Обработчик закрытия уведомления self.addEventListener('notificationclose', event => { console.log('Уведомление было закрыто', event.notification.tag); });На клиенте (например, в
index.htmlили вашем React/Vue компоненте):// Проверяем поддержку API if ('serviceWorker' in navigator && 'PushManager' in window) { // Регистрируем Service Worker при загрузке страницы navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('✅ Service Worker зарегистрирован:', registration); }) .catch(error => { console.error('❌ Ошибка регистрации Service Worker:', error); }); } else { console.warn('⚠️ Web Push API не поддерживается в этом браузере'); }Шаг 3: Запрашиваем разрешение и подписываем пользователя
Теперь нужно запросить у пользователя разрешение на отправку уведомлений и подписать его.
// Публичный VAPID ключ (не секретный, можно хранить в коде) const VAPID_PUBLIC_KEY = process.env.REACT_APP_VAPID_PUBLIC_KEY; // Вспомогательная функция для конвертации Base64 в Uint8Array 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; } // Функция подписки на уведомления async function subscribeToPush() { try { // Ждём, пока Service Worker будет готов const registration = await navigator.serviceWorker.ready; // Запрашиваем разрешение у пользователя // ВАЖНО: это должно быть в ответ на действие пользователя (клик) const permission = await Notification.requestPermission(); if (permission !== 'granted') { console.log('❌ Пользователь отклонил разрешение'); return; } // Подписываем пользователя на push const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, // Уведомления должны быть видны applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) }); console.log('✅ Пользователь подписан на уведомления'); console.log('Subscription:', subscription); // Отправляем данные подписки на сервер await fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(subscription) }); console.log('✅ Данные подписки отправлены на сервер'); } catch (error) { console.error('❌ Ошибка при подписке:', error); } } // Вешаем обработчик на кнопку document.getElementById('subscribe-btn').addEventListener('click', subscribeToPush);Лучшие практики для запроса разрешения:
- Запрашивайте разрешение только после действия пользователя (клик на кнопку)
- Объясните, зачем вам нужны уведомления
- Не спрашивайте сразу же при загрузке страницы — это раздражает
- Предложите возможность отписки позже
Шаг 4: Серверная реализация (Node.js)
На сервере используем библиотеку
web-pushдля отправки уведомлений.Установка зависимостей:
npm install web-push express dotenvФайл
server.js:const express = require('express'); const webpush = require('web-push'); require('dotenv').config(); const app = express(); app.use(express.json()); // Инициализируем web-push с VAPID ключами webpush.setVapidDetails( 'mailto:your-email@example.com', // Ваша почта (может быть любой) process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY ); // Хранилище подписок (в продакшене используйте БД) const subscriptions = new Set(); // Эндпоинт для сохранения подписки пользователя app.post('/api/subscribe', (req, res) => { const subscription = req.body; try { // Сохраняем подписку (в реальном приложении — в БД) subscriptions.add(JSON.stringify(subscription)); res.status(201).json({ success: true, message: 'Подписка сохранена' }); } catch (error) { console.error('Ошибка при сохранении подписки:', error); res.status(500).json({ success: false, message: 'Ошибка сохранения подписки' }); } }); // Функция отправки уведомления конкретному пользователю async function sendNotificationToUser(subscription, payload) { try { await webpush.sendNotification(subscription, JSON.stringify(payload)); console.log('✅ Уведомление отправлено'); } catch (error) { if (error.statusCode === 410) { // Подписка устарела, удаляем её subscriptions.delete(JSON.stringify(subscription)); console.log('⚠️ Подписка удалена (410 Gone)'); } else { console.error('❌ Ошибка отправки:', error); } } } // Функция отправки уведомления всем пользователям async function sendNotificationToAll(payload) { console.log(`📤 Отправляем уведомление ${subscriptions.size} пользователям`); for (const subStr of subscriptions) { const subscription = JSON.parse(subStr); await sendNotificationToUser(subscription, payload); } } // Тестовый эндпоинт для отправки уведомления app.post('/api/send-notification', async (req, res) => { const { title, body, icon, url } = req.body; const payload = { title: title || 'Новое уведомление', body: body || 'Нажмите, чтобы узнать больше', icon: icon || '/icon-192.png', url: url || '/' }; try { await sendNotificationToAll(payload); res.json({ success: true, message: `Уведомление отправлено ${subscriptions.size} пользователям` }); } catch (error) { console.error('Ошибка:', error); res.status(500).json({ success: false, message: 'Ошибка при отправке уведомления' }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🚀 Сервер запущен на http://localhost:${PORT}`); });
Важные особенности для каждого браузера
Ну и конечно же разные браузеры имеют разные требования и ограничения. Об этом нужно помнить!
Браузер Требования Особенности Chrome/Edge Видимые уведомления для каждого push Поддержка до 4KB полезной нагрузки Firefox Видимые уведомления требуются 4KB полезная нагрузка Safari Видимые уведомления обязательны Только 2KB полезная нагрузка, требует веб-приложения iOS (Safari) Не поддерживает background push Уведомления работают только при открытом приложении Всегда отображайте уведомления немедленно при их получении в Service Worker. Некоторые браузеры требуют это для сохранения разрешений.
Ограничения на размер сообщений
Push-уведомления не могут содержать большое количество данных. Если вам нужно отправить много информации:
// ❌ ПЛОХО: отправляем большой JSON const badPayload = { title: 'Новая статья', body: 'Очень длинное описание статьи...', fullArticle: '...' // 100KB текста }; // ✅ ХОРОШО: отправляем минимальные данные const goodPayload = { title: 'Новая статья', body: 'Кликните для прочтения', articleId: '12345' // ID статьи }; // В Service Worker загружаем полные данные: self.addEventListener('push', async event => { const data = event.data?.json() || {}; // Загружаем полные данные с сервера const response = await fetch(`/api/article/${data.articleId}`); const article = await response.json(); // Теперь отображаем с полной информацией event.waitUntil( self.registration.showNotification(article.title, { body: article.description, image: article.coverImage }) ); });
Управление жизненным циклом подписки
Время от времени браузер может автоматически пересоздать подписку (например, после обновления Service Worker). Нужно об этом позаботиться:
// В Service Worker self.addEventListener('pushsubscriptionchange', event => { console.log('⚠️ Подписка изменилась, переподписываемся'); event.waitUntil( self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) }) .then(newSubscription => { // Отправляем новую подписку на сервер return fetch('/api/update-subscription', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newSubscription) }); }) .catch(error => { console.error('❌ Ошибка при переподписке:', error); }) ); });
Безопасность: неотъемлемая часть
Без безопасности никуда, тем более после последних угроз связанных с Next - лучше стараться уделять этому вопросу больше внимания.
А теперь о правилах, золотые правила:
-
Приватный ключ VAPID никогда не передавайте клиенту. Это секрет!
-
Шифруйте полезную нагрузку - Web Push API делает это автоматически, но всегда проверяйте
-
Валидируйте данные на сервере при сохранении подписок
-
Используйте HTTPS - это не опционально
-
Реализуйте Rate Limiting на эндпоинте отправки, чтобы предотвратить спам
-
Удаляйте старые подписки, если браузер возвращает ошибку 410 (Gone)
Для примера:
// Пример простой защиты на сервере const RATE_LIMIT = 5; // Максимум 5 уведомлений в минуту let lastNotificationTime = 0; let notificationCount = 0; app.post('/api/send-notification', (req, res) => { const now = Date.now(); // Сбрасываем счётчик каждую минуту if (now - lastNotificationTime > 60000) { lastNotificationTime = now; notificationCount = 0; } // Проверяем лимит if (notificationCount >= RATE_LIMIT) { return res.status(429).json({ success: false, message: 'Слишком много запросов' }); } notificationCount++; // ... остальной код отправки });Также стоит упомянуть о типичных ошибках и как их решать:
Проблема Причина Решение Service Worker не регистрируется Отсутствует HTTPS (кроме localhost) Используйте HTTPS или localhost для тестирования Браузер не запрашивает разрешение API вызван без действия пользователя Вешайте обработчик на клик кнопки Уведомления не приходят VAPID ключи неправильные Проверьте, что используете одну пару ключей везде Ошибка 410 (Gone) Подписка устарела Удалите подписку из БД, переподпишите пользователя Сообщение слишком большое Превышен лимит (4KB или 2KB) Уменьшите размер payload, отправляйте ID вместо данных
Продвинутые фишки
Возможно кому-то это покажется излишним, но в некоторых случаях такие решения могут облегчить жизнь.
Группировка уведомлений
Используйте параметрtagдля группировки уведомлений одного типа:const options = { body: 'Новое сообщение в чате', tag: 'chat-messages', // Все уведомления с этим тегом объединяются renotify: true // Заново уведомить пользователя };Кнопки действий
Некоторые браузеры поддерживают кнопки в уведомлениях:const options = { body: 'Вам пришел новый заказ', actions: [ { action: 'approve', title: 'Принять' }, { action: 'decline', title: 'Отклонить' } ] }; // В Service Worker обрабатываем действия self.addEventListener('notificationclick', event => { if (event.action === 'approve') { // Логика для принятия } else if (event.action === 'decline') { // Логика для отклонения } });Изображения в уведомлениях
Многие браузеры поддерживают изображения:const options = { body: 'Новая фотография', image: 'https://example.com/photo.jpg', // Большое изображение icon: '/app-icon.png', // Маленькая иконка badge: '/badge-icon.png' // Иконка для Android };
Web Push API - это мощный инструмент, который позволяет создавать действительно стоящие веб-приложения. Да, есть нюансы с разными браузерами и ограничения на размер данных, но это все решаемо.
И помните золотые правила:
Используйте HTTPS (или localhost для тестирования)
Всегда запрашивайте разрешение в ответ на действие пользователя
Генерируйте и сохраняйте VAPID ключи в безопасности
Регулярно удаляйте устаревшие подписки (410 ошибки)
Следите за размером payload — не более 4KB
Тестируйте в DevTools для быстрой отладкиНадеюсь, это руководство было исчерпывающим и поможет вам разобраться с Web Push API. Если у вас возникнут вопросы - добро пожаловать на наш форум.
Не стесняйтесь и задавайте вопросы.И конечно удачи в разработке
!
© 2024 - 2025 ExLends, Inc. Все права защищены.