Перейти к содержанию
  • Лента
  • Категории
  • Последние
  • Метки
  • Популярные
  • Пользователи
  • Группы
Свернуть
exlends
Категории
  1. Главная
  2. Категории
  3. Языки программирования
  4. JavaScript
  5. Web Push API: Полное руководство по реализации push-уведомлений

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

Запланировано Прикреплена Закрыта Перенесена JavaScript
javascript
1 Сообщения 1 Постеры 39 Просмотры
  • Сначала старые
  • Сначала новые
  • По количеству голосов
Ответить
  • Ответить, создав новую тему
Авторизуйтесь, чтобы ответить
Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.
  • kirilljsxK Не в сети
    kirilljsxK Не в сети
    kirilljsx
    js
    написал в отредактировано
    #1

    Присаживайте по удобнее, заварите чайку тема будет объемная и большая…

    А делиться я буду своим опытом реализации 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. Если у вас возникнут вопросы - добро пожаловать на наш форум.
    Не стесняйтесь и задавайте вопросы.

    И конечно удачи в разработке 😊 !

    1 ответ Последний ответ
    1

    Категории

    • Главная
    • Новости
    • Фронтенд
    • Бекенд
    • Языки программирования

    Контакты

    • Сотрудничество
    • info@exlends.com
    • Наш чат
    • Наш ТГ канал

    © 2024 - 2025 ExLends, Inc. Все права защищены.

    Политика конфиденциальности
    • Войти

    • Нет учётной записи? Зарегистрироваться

    • Войдите или зарегистрируйтесь для поиска.
    • Первое сообщение
      Последнее сообщение
    0
    • Лента
    • Категории
    • Последние
    • Метки
    • Популярные
    • Пользователи
    • Группы