@karpo518

Как правильно работать с web push уведомлениями?

Я осваиваю технологию web push уведомлений. Экспериментирую с кодом на основе небольшого javascript-кода. Я взял основу для него в репозитории https://github.com/Minishlink/web-push-php-example Я обнаружил в коде подозрительную логику и хотел бы прояснить ситуацию.

1. По событию DOMContentLoaded выполняется регистрация воркера. Неважно, в первый раз запускается скрипт или в 100-ый. Он каждый раз регистрирует воркер. Является ли это оптимальным решением? Ведь воркер должен храниться в памяти браузера. Проще было бы сперва проверить, не зарегистрирован ли уже этот воркер.

2. При регистрации воркера вызывается функция push_updateSubscription, которая следит за изменением токена после регистрации. Я не знаю, что за сценарии могут привести к изменению этого токена, но понимаю, что каждая загрузка страницы при такой логике будет приводить к обращению к БД для обновления токена подписки. Такая логика выглядит совсем не оптимально. Как это должно работать в действительности?

3. Правильно ли я понял, что при обновлении подписки мы идентифицируем пользователя по endpoint url, который заменяет нам id устройства?

Тестовый скрипт
document.addEventListener('DOMContentLoaded', () => {
  const applicationServerKey =
    'публичный_ключ';
  let isPushEnabled = false;

  if (!('serviceWorker' in navigator)) {
    console.warn('Service workers are not supported by this browser');
    //changePushButtonState('incompatible');
    return;
  }

  if (!('PushManager' in window)) {
    console.warn('Push notifications are not supported by this browser');
    //changePushButtonState('incompatible');
    return;
  }

  if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
    console.warn('Notifications are not supported by this browser');
    //changePushButtonState('incompatible');
    return;
  }

  // Check the current Notification permission.
  // If its denied, the button should appears as such, until the user changes the permission manually
  if (Notification.permission === 'denied') {
    console.warn('Notifications are denied by the user');
    //changePushButtonState('incompatible');
    return;
  }

  navigator.serviceWorker.register('/local/web-push/serviceWorker.js').then(
    async() => {
      console.log('[SW] Service worker has been registered');
      let result = await push_updateSubscription();
      console.log(result);
    },
    e => {
      console.error('[SW] Service worker registration failed', e);
      //changePushButtonState('incompatible');
    }
  );

  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 checkNotificationPermission() {
    return new Promise((resolve, reject) => {
      if (Notification.permission === 'denied') {
        return reject(new Error('Push messages are blocked.'));
      }

      if (Notification.permission === 'granted') {
        return resolve();
      }

      if (Notification.permission === 'default') {
        return Notification.requestPermission().then(result => {
          if (result !== 'granted') {
            reject(new Error('Bad permission result'));
          }

          resolve();
        });
      }
    });
  }

  async function push_subscribe() {
    //changePushButtonState('computing');

    return checkNotificationPermission()
      .then(() => navigator.serviceWorker.ready)
      .then(serviceWorkerRegistration =>
        serviceWorkerRegistration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
        })
      )
      .then(subscription => {
        // Subscription was successful
        // create subscription on your server
        return push_sendSubscriptionToServer(subscription, 'POST');
      })
      .catch(e => {
        if (Notification.permission === 'denied') {
          // The user denied the notification permission which
          // means we failed to subscribe and the user will need
          // to manually change the notification permission to
          // subscribe to push messages
          console.warn('Notifications are denied by the user.');
          changePushButtonState('incompatible');
        } else {
          // A problem occurred with the subscription; common reasons
          // include network errors or the user skipped the permission
          console.error('Impossible to subscribe to push notifications', e);
          changePushButtonState('disabled');
        }
      });
  }

  

  async function push_updateSubscription() {
    return navigator.serviceWorker.ready
      .then(serviceWorkerRegistration => serviceWorkerRegistration.pushManager.getSubscription())
      .then(subscription => {

        if (!subscription) {
          return push_subscribe();
          
        }

        // Keep your server in sync with the latest endpoint
        return push_sendSubscriptionToServer(subscription, 'PUT');

      })
      .catch(e => {
        console.error('Error when updating the subscription', e);
      });
  }

  async function push_sendSubscriptionToServer(subscription, method) {
      
      const key = subscription.getKey('p256dh');
      const token = subscription.getKey('auth');
      const contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];

      return fetch('/local/web-push/push_subscription.php', {
          method,
          body: JSON.stringify({
            endpoint: subscription.endpoint,
            publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
            authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
            contentEncoding,
          }),
      });
  }

});
  • Вопрос задан
  • 176 просмотров
Пригласить эксперта
Ответы на вопрос 2
glaphire
@glaphire
PHP developer
1. В демке пошли по пути наименьшего сопротивления, т.е. в своем приложении можно устанавливать свои правила, когда надо регистрировать воркер. Понимание тонкостей JS и его сервис воркеров не моя сильная сторона, но воркер регистрируется единожды после того, как его вызвали и живет, пока его не убили (вручную или кодом). В моем приложении на продакшене с этим проблем не было)
2. Сценарий - полная очистка кеша браузера (т.е. удалелие сервис воркера и эндпоинта подписки). Такое нужно, если вы хотите считать такого пользователя и дальше своим старым подписчиком, а не новым, "с нуля".
3. Да
Ответ написан
zavoloklom
@zavoloklom
Fullstack разработчик
Перерегистрация сервисворкера обусловлена тем что сервисворкер должен быть всегда "свежим". Вы можете настроить перерегистрацию только когда код сервисворкера действительно поменялся, например сравнивая версию своего кода или каким-то иным способом.

Обновление данных необходимо в случае, когда пользователь очищает кэш браузера и делает unregister сервисворкеру.
Таким образом пользователю при повторном заходе выпишутся новые ключи вместо невалидных старых.

Endpoint url не является идентификатором устройства. Если вам необходимо отслеживать что подписавшееся устройство ранее уже было подписано - используйте специальные библиотеки (например fingerprint) и передавая отпечаток вместе с подпиской сопоставляйте данные на бэкенде.

Однако старая подписка устройства становится недействительной при появлении новой, поэтому не бойтесь что на устройство придет несколько пушей.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы