Push уведомления

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


Регистрация Service Worker

Service Worker выполняет роль фонового скрипта, который перехватывает события push и взаимодействует с браузером. Для регистрации Service Worker используется метод navigator.serviceWorker.register():

if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(registration => {
      console.log('Service Worker зарегистрирован:', registration);
    })
    .catch(error => {
      console.error('Ошибка регистрации Service Worker:', error);
    });
}

Ключевые моменты:

  • Файл service-worker.js должен находиться в корне проекта или на уровне, позволяющем покрывать нужные URL.
  • Service Worker работает только через HTTPS (за исключением локального localhost).

Подписка на Push

Для получения push-уведомлений необходимо создать подписку через PushManager:

function subscribeUser() {
  navigator.serviceWorker.ready.then(registration => {
    const subscribeOptions = {
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array('<VAPID_PUBLIC_KEY>')
    };

    return registration.pushManager.subscribe(subscribeOptions);
  })
  .then(pushSubscription => {
    console.log('Подписка создана:', JSON.stringify(pushSubscription));
  });
}

Особенности:

  • userVisibleOnly: true гарантирует, что уведомление всегда будет отображаться пользователю.
  • applicationServerKey используется для аутентификации на сервере через протокол VAPID. Этот ключ преобразуется из 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);
  return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}

Обработка push-событий в Service Worker

После подписки браузер может получать push-события. Service Worker обрабатывает их с помощью слушателя push:

self.addEventListener('push', event => {
  const data = event.data ? event.data.json() : {};
  const title = data.title || 'Новое уведомление';
  const options = {
    body: data.body || 'Текст уведомления',
    icon: '/icons/notification-icon.png',
    badge: '/icons/notification-badge.png',
    data: data.url || '/'
  };

  event.waitUntil(
    self.registration.showNotification(title, options)
  );
});

Основные аспекты:

  • Метод showNotification отвечает за отображение уведомления.
  • Свойство data позволяет передавать дополнительную информацию, например ссылку для открытия при клике.

Обработка кликов по уведомлениям

Для реализации навигации при клике на уведомление используется событие notificationclick:

self.addEventListener('notificationclick', event => {
  event.notification.close();
  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => {
      const url = event.notification.data;
      for (const client of clientList) {
        if (client.url === url && 'focus' in client) return client.focus();
      }
      if (clients.openWindow) return clients.openWindow(url);
    })
  );
});

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

  • Сначала уведомление закрывается методом close().
  • Затем выполняется попытка сфокусировать уже открытое окно с нужным URL или открыть новое.

Интеграция с компонентами Polymer

В Polymer push-уведомления удобно инкапсулировать в отдельный элемент. Пример простого элемента push-manager:

import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';

class PushManagerElement extends PolymerElement {
  static get template() {
    return html`<button on-click="subscribeUser">Подписаться на уведомления</button>`;
  }

  subscribeUser() {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;

    navigator.serviceWorker.ready.then(registration => {
      registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('<VAPID_PUBLIC_KEY>')
      }).then(subscription => {
        console.log('Подписка создана:', JSON.stringify(subscription));
        // Здесь можно отправить subscription на сервер
      });
    });
  }
}
customElements.define('push-manager-element', PushManagerElement);

Преимущества такого подхода:

  • Компонент можно переиспользовать в разных страницах.
  • Логика подписки и UI находятся в одном месте.
  • Обновления и изменения проще тестировать и масштабировать.

Серверная часть для отправки уведомлений

Для отправки push-сообщений сервер использует подписку пользователя. На Node.js это реализуется через библиотеку web-push:

const webPush = require('web-push');

webPush.setVapidDetails(
  'mailto:example@domain.com',
  '<VAPID_PUBLIC_KEY>',
  '<VAPID_PRIVATE_KEY>'
);

const pushSubscription = { /* объект подписки пользователя */ };
const payload = JSON.stringify({ title: 'Привет', body: 'Новое уведомление' });

webPush.sendNotification(pushSubscription, payload)
  .then(response => console.log('Уведомление отправлено', response))
  .catch(error => console.error('Ошибка отправки', error));

Особенности:

  • Каждая подписка уникальна и связана с конкретным пользователем и браузером.
  • Payload может быть ограничен по размеру (обычно до 4 КБ).

Рекомендации по безопасности и UX

  • Запрашивать разрешение на уведомления только при явной необходимости.
  • Обрабатывать возможный отказ пользователя без ошибок.
  • Использовать push для действительно важных сообщений, чтобы не раздражать пользователя.
  • Обновлять подписки при изменении ключей VAPID или длительных периодах без активности.

Эта схема позволяет создавать надежные, масштабируемые push-уведомления в веб-приложениях на Polymer, обеспечивая современный пользовательский опыт и высокую модульность кода.