Привет, ребята, мы только что реализовали Web Push в наших примерах списков приложений https://lists.state-less.cloud и хотели поделиться процессом.

Что такое React-сервер

Если вы еще не слышали об этом, React Server — это новая платформа, которая позволяет создавать серверные компоненты с использованием JSX/TSX вместе с реактивным стилем кодирования, известным из библиотеки React. (компоненты, хуки, эффекты и т. д.)

Платформа обеспечивает двустороннюю привязку от внешних компонентов к внутренним с использованием GraphQL с подписками в качестве транспортного уровня.

Вы можете передать любые реквизиты с сервера клиенту и использовать их, используя простой крючок useComponent во внешнем интерфейсе. Передача функций во внешний интерфейс позволяет вам вызывать серверный код непосредственно из внешнего интерфейса.

Компоненты React на стороне сервера упрощают согласование общей архитектуры серверных и внешних компонентов.

Использование подхода React к архитектуре кода позволяет легко создавать реактивные и серверные компоненты/приложения с плавной двусторонней привязкой данных (обновление состояния на сервере обновляет состояние всех подключенных клиентов). Это упрощает и упрощает проверку вызовов функций на стороне сервера.

Дополнительную информацию можно найти в нашей документации: https://state-less.cloud.

Списки

Мы создали простой пример приложения под названием Списки, который позволяет создавать списки, задачи и счетчики и управлять ими. Если вам интересно, попробуйте и поделитесь своими отзывами или идеями. Это позволяет React Server адаптироваться и расти в соответствии с потребностями сообщества.

Приветственной функцией является наличие push-уведомлений, которые напоминают вам о необходимости выполнения определенных задач.

Веб-пуш

Демо

Если вы хотите опробовать push-уведомления на своем устройстве, перейдите по адресу https://state-less.cloud/lists?fs, создайте новый список, добавьте элемент и настройте его, включив режим редактирования списков и нажав три точки справа от элемента списка. Откроется меню, в котором вы можете выбрать время. Введите время менее 15 минут, чтобы вызвать уведомление. Закройте меню и включите push-уведомления, нажав кнопку уведомления вверху. Это должно немедленно отправить вам приветственное уведомление.

Выполнение

Если вы уже работали с push-уведомлениями, то знаете, что нужно позаботиться о многом, чтобы правильно реализовать работающую систему push-уведомлений.

Давайте посмотрим на стек проектов и на то, что нам нужно. Web Push требует от вас использования сервисных работников.

Мы используем vite в качестве инструментов внешнего интерфейса и React в качестве фреймворка. К счастью, в Vite проще добавлять сервис-воркеров, чем в CRA.

Существует плагин под названием VitePWA, который помогает вам настраивать сервис-воркеров. Обратите внимание: вам необходимо включить режим разработки, если вы хотите протестировать его локально.

Обновите свой vite.config.ts , используя следующий код.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    port: 3001,
  },
  envPrefix: 'REACT_APP_',
  plugins: [
    react(),
    VitePWA({
      srcDir: 'src',
      registerType: 'autoUpdate',
      filename: 'service-worker.js',
      strategies: 'injectManifest',
      injectRegister: 'auto',
      manifest: false,
      injectManifest: {
        injectionPoint: null,
      },
      devOptions: {
        enabled: true,
        type: 'module',
      },
    }),
  ],
});

Чтобы зарегистрировать сервис-воркера, вам также необходимо добавить следующий код в файл main.tsx.

import { registerSW } from 'virtual:pwa-register';

if ('serviceWorker' in navigator) {
  registerSW();
}

К счастью, VitePWA все еще создает файл service-worker.js, поэтому он уже доступен в рабочей среде. При локальном тестировании вам необходимо настроить VitePWA для сборки во время разработки. В результате будет создана папка dev-dist, содержащая ваш файл serviceworker.

Заключение

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

Код внешнего интерфейса

Наш интерфейсный код состоит из сервис-воркера и элемента пользовательского интерфейса для подписки/отписки от push-уведомлений.

Сервисный работник

Не зацикливайтесь на коде сервис-воркера. Он содержит дополнительную логику для отображения кнопки внутри уведомления для выполнения некоторых действий на сервере. (Мы используем код из нашего приложения «Списки», и пользователи могут завершить элемент задачи, связанный с уведомлением, нажав кнопку).

Если вам не нужно отображать какие-либо кнопки действий, вы можете смело опустить часть кода, которая содержит действия и обрабатывает события notificationclick.

let lastNotificationData = {};
self.addEventListener('push', function (event) {
  if (event.data) {
    try {
      const json = JSON.parse(event.data.text());
      const options = {};
      if (json.actions.includes('complete')) {
        Object.assign(options, {
          actions: [
            {
              action: `complete:${json.id}`,
              type: 'button',
              title: 'Complete',
            },
          ],
        });
      }
      lastNotificationData[json.id] = json;
      showLocalNotification(json.title, json.body, self.registration, options);
    } catch (error) {
      showLocalNotification('Error', event.data.text(), self.registration);
    }
  } else {
    console.log('Push event but no data');
  }
});

self.addEventListener('notificationclick', (event) => {
  const clickedNotification = event.notification;
  clickedNotification.close();
  const [action, id] = event.action.split(':');
  const { token, clientId } = lastNotificationData[id];
  delete lastNotificationData[id];
  
  const bearer = `Bearer ${token}`;

  if (action === 'complete') {
    try {
      const promise = fetch(`http://localhost:4000/todos/${id}/toggle`, {
        method: 'GET',
        headers: {
          Authorization: bearer,
          'X-Unique-Id': clientId,
        },
      });
      event.waitUntil(promise);
    } catch (e) {
      console.log('SW: error completing', e);
    }
  }
});

const showLocalNotification = (title, body, swRegistration, opt = {}) => {
  const options = {
    body,
    ...opt,
    // here you can add more properties like icon, image, vibrate, etc.
  };
  swRegistration.showNotification(title, options);
};

Давайте подробнее рассмотрим, что делает код.

self.addEventListener('push', function (event) {
   // Handle incoming push notifications
})

Добавление прослушивателя событий для события push дает указание сервисному работнику выполнить вашу функцию обратного вызова, когда он получает сообщение, например. через ФСМ.

Это работает, даже если пользователь закрыл страницу. Чтобы отображать уведомления с помощью встроенных push-уведомлений, вам необходимо использовать метод Service Workers showNotification.

Чтобы иметь возможность управлять заголовком, телом и связывать сущность на серверной стороне с уведомлением, имеет смысл использовать JSON и отправлять строковый объект в качестве полезной нагрузки в конечную точку push. Затем во внешнем интерфейсе вы можете просто проанализировать полезную нагрузку и отобразить заголовок и тело соответственно.

const json = JSON.parse(event.data.text());
// { title, body, id }

На данный момент это все, что нам нужно во фронтенде, давайте взглянем на серверную часть нашего приложения.

Бэкэнд

Что нам нужно?

  • ВАПИД-ключи
  • Конечная точка подписки
  • уведомить конечную точку
  • конечная точка действия

Примечание. Чтобы использовать WebPush, вам необходимо шифровать отправляемые сообщения. Библиотека web-push делает это автоматически, все, что вам нужно сделать, это инициализировать библиотеку с помощью вашего закрытого и открытого ключа.

ВАПИД-ключи

Чтобы сгенерировать новую пару ключей, вы можете установить web-push глобально и выполнить web-push generate-vapid-keys . Это создаст и зарегистрирует новую пару ключей, которую вам следует скопировать и ввести в свой файл .env.

VAPID_PRIVATE="zR_Gyj6Xlqs1gA4scASenSuECo59QLQoqbjlA46vEjU"
VAPID_PUBLIC="BEV7u6o8h_8IXBfTvkUKTKpIE2l-e1DDfkR33jXi_Fservice-worker.jsR1IeZ_SG0FA1bmmfvaxiEG90asoFmwKSqAzF-Nb9h0"

Мы будем использовать переменные среды в нашем внутреннем коде для инициализации библиотеки web-push и использовать серверный компонент для отправки открытого ключа во внешний интерфейс.

Инициализация

Если вы еще не настроили проект сервера реагирования, вы можете инициализировать новый, используя этот код.

git clone https://github.com/state-less/clean-starter.git my-server
cd my-server
git remote remove origin
yarn install
yarn start

Это клонирует репо и установит зависимости. Откройте или создайте файл my-server\src\comComponents\WebPushManager.tsx. Мы будем использовать его для создания серверного компонента, который предоставляет интерфейсу несколько функций, таких как подписаться, отписаться, sendNotification, vapid.

Скопируйте и вставьте следующий код в свой файл.

import {
    authenticate,
    isClientContext,
    useState,
} from '@state-less/react-server';
import webpush from 'web-push';
import { JWT_SECRET, VAPID_PUBLIC, VAPID_PRIVATE } from '../config';
import { ServerSideProps } from './ServerSideProps';
import { notificationEngine } from '../instances';
import logger from '../lib/logger';

webpush.setVapidDetails(
    '[email protected]',
    VAPID_PUBLIC,
    VAPID_PRIVATE
);

export const WebPushManager = (props, { key, context }) => {
    let user = null;
    if (isClientContext(context))
        try {
            user = authenticate(context.headers, JWT_SECRET);
        } catch (e) {}

    const clientId = context.headers?.['x-unique-id'] || 'server';

    const [subscribed, setSubscribed] = useState(
        {},
        {
            key: 'subscribed',
            scope: key,
        }
    );

    const subscribe = (subscription) => {
        setSubscribed({
            ...subscribed,
            [clientId]: {
                sub: JSON.parse(subscription),
                user,
            },
        });
        notificationEngine.subscribe(clientId, user, JSON.parse(subscription));
    };

    const unsubscribe = () => {
        setSubscribed({
            ...subscribed,
            [clientId]: null,
        });
        notificationEngine.unsubscribe(clientId, user);
    };

    const sendNotification = (body) => {
        const { sub } = subscribed?.[clientId] || {};
        if (typeof body !== 'string') {
            body = JSON.stringify(body);
        }
        if (sub) {
            try {
                webpush.sendNotification(sub, body);
            } catch (e) {
                logger.error`Error sending notification`;
            }
        }
    };

    return (
        <ServerSideProps
            subscribe={subscribe}
            unsubscribe={unsubscribe}
            sendNotification={sendNotification}
            vapid={VAPID_PUBLIC}
            key={`${key}-props`}
        />
    );
};

После создания файла вам необходимо отобразить серверный компонент в вашем index.tsx, найти тег <Server> и добавить компонент <WebPushManager key="web-push" />. Это отобразит компонент на сервере и предоставит его интерфейсу.

Запустите свой сервер и перейдите к коду внешнего интерфейса.

yarn start

Фронтенд, часть II (Подключение к бэкэнду)

Теперь, когда серверная часть готова, нам нужно подписаться на push-уведомления на веб-интерфейсе и отправить подписку на серверную часть для ее хранения.

Серверная часть может позднее получить сохраненную подписку и использовать ее для отправки уведомлений клиенту с помощью библиотеки web-push.

Давайте создадим компонент NotificationButton, который будет обрабатывать всю логику подписки и отображения состояния уведомления.

Откройте или создайте файл lists-app-frontend/src/comComponents/NotificationButton.tsx и просмотрите следующий код.

import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import { IconButton } from '@mui/material';
import { useComponent, useLocalStorage } from '@state-less/react-client';
import { useEffect } from 'react';

const requestNotificationPermission = async () => {
  const permission = await window.Notification.requestPermission();
  return permission;
};

export const NotificationButton = () => {
  const [pushManager] = useComponent('web-push');
  const [permission, setPermission] = useLocalStorage<{
    notification: string | null;
    subscription: boolean | null;
  }>('permission', {
    notification: null,
    subscription: null,
  });

  useEffect(() => {
    (async () => {
      const reg = await navigator.serviceWorker.getRegistration();
      const sub = await reg?.pushManager.getSubscription();
      setPermission({
        notification: Notification.permission,
        subscription: !!sub,
      });
    })();
  }, []);

  const toggleNotifications = async () => {
    const reg = await navigator.serviceWorker.getRegistration();
    const sub = await reg?.pushManager.getSubscription();

    if (
      Notification.permission !== 'granted' ||
      (Notification.permission === 'granted' && !sub)
    ) {
      const perm = await requestNotificationPermission();
      if (perm === 'granted') {
        if (!sub) {
          const sub = await reg?.pushManager.subscribe({
            applicationServerKey: pushManager.props.vapid,
            userVisibleOnly: true,
          });
          await pushManager.props.subscribe(JSON.stringify(sub));
          setPermission({
            ...permission,
            subscription: true,
          });
        }
        await pushManager.props.sendNotification({
          title: 'Welcome to Lists',
          body: 'You have been granted permission to receive notifications',
        });
      } else {
        setPermission({
          notification: perm,
          subscription: !!sub,
        });
      }
    } else {
      await pushManager.props.sendNotification({
        title: 'Goodbye',
        body: "You won't receive any more notifications.",
      });
      setTimeout(async () => {
        const res = await sub?.unsubscribe();
        setPermission({
          ...permission,
          subscription: false,
        });
        await pushManager.props.unsubscribe(JSON.stringify(sub));
      }, 1000);
    }
  };

  return (
    <IconButton
      disabled={permission.notification === 'denied'}
      color={
        permission.notification === 'granted'
          ? permission.subscription
            ? 'success'
            : 'warning'
          : undefined
      }
      sx={{ ml: 'auto' }}
      onClick={(e) => (toggleNotifications(), void 0)}
    >
      <NotificationsNoneIcon />
    </IconButton>
  );
};

Мы используем наш серверный компонент web-push с помощью хука useComponent. Это позволяет нам получить открытый ключ VAPID, который мы ранее передали нашему серверному компоненту. Он также предоставляет две функции для подписки и отказа от подписки на уведомления.

Заключение

Клонируя репозитории и следуя этой статье, вы должны хорошо понимать, как реализовать функцию push-уведомлений.

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

Через минуту вы должны получить уведомление с предложением выполнить связанный элемент. Если вы нажмете на кнопку в уведомлении, вы сможете завершить пункт.

Если вам удалось довести дело до конца, у вас должно быть работающее приложение для управления списками с push-уведомлениями.

Вы можете найти репозитории для этого поста здесь:





Приятного программирования и не забудьте заглянуть в наши социальные сети:
Github, Twitter, Discord

На простом английском языке

Спасибо, что вы являетесь частью нашего сообщества! Прежде чем уйти: