Привет! В этой статье мы разберемся, как отправлять push-уведомления пользователям iOS, даже если ваше приложение временно недоступно в App Store. С выходом Safari 16.4, появилась возможность получать уведомления в Progressive Web Apps (PWA)

Давайте разберем эту задачу со строны фронтенд‑разработчика

Что нам понадобится

Серверная часть: для серверной логики выберем Node.js.

Клиентская часть: мы будем использовать React.js для создания пользовательского интерфейса.

Push-сервис: в качестве сервиса для отправки push-уведомлений используем Google Cloud Messaging.

Взаимодействие сервисов
Взаимодействие сервисов

Создание сервера на Express.js

Давайте начнем с создания VAPID ключей. Чтобы понять, зачем они нужны, кратко пройдемся по теории. VAPID — это, по сути, уникальный ID для вашего приложения в мире веб-пуш-уведомлений. Эти ключи помогут браузеру понять, откуда идет уведомление, и обеспечат дополнительный слой безопасности. Итак, у нас будет пара ключей публичный и приватный

Теперь практика! Мы будем использовать библиотеку для Node.js под названием web-push. Эта библиотека хорошо работает с Google Cloud Messaging, системой от Google для отправки уведомлений

Устанавливаем библиотеку c помощью npm

npm install web-push -g

Генерируем ключи

web-push generate-vapid-keys

Мы сгенерировали два ключа: публичный и приватный. Публичный ключ мы будем использовать на клиентской стороне, когда пользователи будут подписываться на уведомления. Этот ключ позволит браузеру идентифицировать источник уведомлений, чтобы убедиться, что они идут от доверенного сервера.

Приватный ключ будет храниться на нашем сервере. Он нужен для подписи данных, которые мы отправляем, и для проверки подлинности нашего приложения перед системами отправки уведомлений, такими как Google Cloud Messaging.

Создадим два url-адреса: один для сохранения подписок на уведомления, а другой для их отправки.

// Подключаем библиотеку web-push для работы с push-уведомлениями
const webPush = require('web-push');

webPush.setVapidDetails(
    publicKey,
    privateKey
);

// Инициализация объекта для хранения подписок
let subscriptions = {}

// Роут для подписки на push-уведомления
app.post('/subscribe', (req, res) => {
    // Извлекаем подписку и ID из запроса
    const {subscription, id} = req.body;
    // Сохраняем подписку в объекте под ключом ID
    subscriptions[id] = subscription;
    // Возвращаем успешный статус
    return res.status(201).json({data: {success: true}});
});

// Роут для отправки push-уведомлений
app.post('/send', (req, res) => {
    // Извлекаем сообщение, заголовок и ID из запроса
    const {message, title, id} = req.body;
    // Находим подписку по ID
    const subscription = subscriptions[id];
    // Формируем payload для push-уведомления
    const payload = JSON.stringify({ title, message });
    
    // Отправляем push-уведомление
    webPush.sendNotification(subscription, payload)
    .catch(error => {
        // В случае ошибки возвращаем статус 400
        return res.status(400).json({data: {success: false}});
    })
    .then((value) => {
        // В случае успешной отправки возвращаем статус 201
        return res.status(201).json({data: {success: true}});
    });
});

Настройка клиентской части

Здесь мы будем работать с базовым приложением на React. Чтобы сделать всё как можно проще, мы создадим свой хук это поможет нам упростить работу с пуш-уведомлениями. Я оставлю комментарии, чтобы было понятно, как всё устроено.

// Функция для преобразования Base64URL в Uint8Array
const 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;
};

// Хук для подписки на push-уведомления
const useSubscribe = ({ publicKey }) => {
    const getSubscription = async () => {
        // Проверка поддержки ServiceWorker и PushManager
        if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
            throw { errorCode: "ServiceWorkerAndPushManagerNotSupported" };
        }

        // Ожидание готовности Service Worker
        const registration = await navigator.serviceWorker.ready;

        // Проверка наличия pushManager в регистрации
        if (!registration.pushManager) {
            throw { errorCode: "PushManagerUnavailable" };
        }

        // Проверка на наличие существующей подписки
        const existingSubscription = await registration.pushManager.getSubscription();
        
        if (existingSubscription) {
            throw { errorCode: "ExistingSubscription" };
        }

        // Преобразование VAPID-ключа для использования в подписке
        const convertedVapidKey = urlBase64ToUint8Array(publicKey);
        return await registration.pushManager.subscribe({
            applicationServerKey: convertedVapidKey,
            userVisibleOnly: true,
        });
    };

    return {getSubscription};
};

Пример использования React хука для получения объекта подписки и отправки его на сервер

// Импорт функции useSubscribe и задание публичного ключа (PUBLIC_KEY)
const { getSubscription } = useSubscribe({publicKey: PUBLIC_KEY});

// Обработчик для подписки на push-уведомления
const onSubmitSubscribe = async (e) => {
    try {
        // Получение объекта подписки с использованием функции getSubscription
        const subscription = await getSubscription();

        // Отправка объекта подписки и ID на сервер для регистрации подписки
        await axios.post('/api/subscribe', {
            subscription: subscription,
            id: subscribeId
        });

        // Вывод сообщения в случае успешной подписки
        console.log('Subscribe success');
    } catch (e) {
        // Вывод предупреждения в случае возникновения ошибки
        console.warn(e);
    }
};

Так большую часть мы уже успешно сделали, теперь нам нужно отобразить уведомление, которое прилетит от Google Cloud Messaging.

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

Для реализации фонового отслеживания новых уведомлений в нашем приложении мы будем использовать две ключевые технологии: сервис-воркер и Push API.

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

Push API — это веб API, которое позволяет серверам отправлять информацию напрямую к браузеру пользователя.

Пример service-worker.js

// Добавляем слушателя события 'push' к сервис-воркеру.
self.addEventListener('push', function(event) {
    // Извлекаем данные из push-события
    const data = event.data.json();

    // Опции для уведомления
    const options = {
        // Текст сообщения в уведомлении
        body: data.message,
        // Иконка, отображаемая в уведомлении
        icon: 'icons/icon-72x72.png'
    };

    // Используем waitUntil для того, чтобы удерживать сервис-воркер активным
    // пока не будет показано уведомление
    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

Демо

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

Благодарю за внимание :-)

Комментарии (0)