Web PUSH Notifications швидко і просто

Web PUSH Notifications швидко і просто
20 хв. читання
01 листопада 2019

У цій замітці я хочу розповісти як швидко і просто налаштувати push-повідомлення на вашому сайті. Ця стаття ні в якому разі не претендує на звання вичерпного керівництва, але, я сподіваюся, що вона дасть точку старту для подальшого вивчення.

Інформації по цій темі в інтернеті повно, але вона фрагментована, розкидана по різних ресурсам і перемішана з повідомленнями для мобільних пристроїв з прикладами на Java, C++ і Python. Нас же, як веб-розробників, цікавить JavaScript. У цій статті я постараюся закумулювати всю необхідну та корисну інформацію.

Web PUSH Notifications швидко і просто

Я думаю, ви вже знаєте, що таке push-повідовлення, але я все ж напишу коротко про головне.

Користувач, заходячи на сайт, витягує (pull) з нього дані. Це зручно і безпечно, але з розвитком інтернет ресурсів, з'явилася необхідність оперативно доставляти інформацію користувачам не чекаючи, поки ті самі зроблять запит. Так і з'явилася технологія примусової доставки (push) даних з сервера клієнту.

Важливо

Push-повідомлення працюють тільки якщо у вас на сайті є HTTPS.
Без валідного SSL сертифікату запустити не вийде. Так що якщо у вас ще немає підтримки HTTPS, то прийшов час її зробити. Рекомендую скористатися let's Encrypt.
Для запуску на localhost потрібно вдаватися до хитрощів. Я тестував скрипти на Github Pages.

Зміст

Хороші повідомлення

Відразу хочу обмовитися, що push-повідомлення не для рекламних розсилок. Відправляти потрібно тільки те, що дійсно потрібно конкретному користувачеві і на що він дійсно повинен оперативно відреагувати.

Хороший приклад:

  • Надсилання повідомлення про зміну статусу звернення користувача до служби техпідтримки;
  • Надсилання повідомлення про зміну статусу замовлення;
  • Поява на складі товару, який чекав користувач;
  • Відповіли на коментар користувача до статті;
  • Нова задача в багтрекері зі статусом Bug або Critical.

Поганий приклад:

  • Нові надходження на склад;
  • Знижки та акції на товари;
  • Нова стаття на сайті;
  • Відповіли на коментар користувача до статті, яку він написав рік тому.

Погані приклади теж вимагають повідомлення, але на них не потрібно реагувати оперативно. Ці повідомлення можна відправити на пошту. Взагалі, всі важливі повідомлення рекомендується дублювати на пошту, так-як push-повідомлення можуть не  дійти до користувача з різних, не залежних від вас причин. Також важливим фактором є актуальність події. Про це я ще поговорю трохи пізніше. Рекомендую до прочитання:

Повернемося до наших баранів. Так як же все це працює? Для початку трохи теорії.

Теорія

Серед непосвячених існує думка що push-повідомлення це проста технологія, яка не потребує для реалізації особливих ресурсів. Насправді ж це цілий пул технологій.

Для початку невелика схема того, як все це працює (анімована схема):

Схема взаємодії у PUSH Notifications

  1. Сервер віддає сторінки користувачеві;
  2. Клієнт підключається до сервера повідомлень, реєструється і отримує ID;
  3. Клієнт відправляє отриманий ID на сервер і сервер прив'язує користувача до конкретного пристрою використовуючи ID пристрої;
  4. Сервер надсилає повідомлення клієнту через сервер повідомлень використовуючи отриманий раніше ID.

На жаль, мені не вдалося з'ясувати, хто і як створює ID пристрою і як сервер повідомлень прив'язується до конкретного пристрою. Я використовував сервер повідомлень Firebase Cloud Messaging від Google і його бібліотеку. На жаль, я не зміг з'ясувати, чи можна його замінити на свій сервер і як це зробити.

Кумедний факт

Спочатку для відправки повідомлень використовували:
Cloud to Device Messaging

Потім його замінили на:
Google Cloud Messaging

А потім ще раз поміняли на:
Firebase Cloud Messaging

Цікаво, що далі.

Що ж відбувається на стороні клієнта?

  • JavaScript запитує у користувача дозвіл на показ повідомлень;
  • Якщо користувач схвалив, то підключаємося до сервера повідомлень і отримуємо ID пристрою;
  • Відправляємо ідентифікатор на наш сервер, щоб ми ідентифікували користувача;
  • Ініціалізуємо JavaScript воркер, який буде працювати в фоні, отримувати повідомлення від сервера повідомлень і показувати їх користувачеві;
  • Підключаємося до сервера повідомлень і чекаємо нових надходжень.

Запит прав на показ повідомлень

Примітка

Google рекомендує використовувати перемикач для підписки і відписки від повідомлень. Таким чином, ініціація процедури підписки на повідомлення виходить від користувача, а не від сайту.
Примусово підписувати користувача на повідомлення, це погана практика. Не робіть так.

Це все виглядає дуже складно, але на сервері все не простіше.

Складності на серверній стороні

  • Зрозуміло, що ідентифікатор пристрою, який присилає користувач, ми зберігаємо в базу даних;
  • Ідентифікатор пристрою добре б прив'язувати до користувача, щоб відправляти персоналізовані повідомлення;
  • Варто пам'ятати, що користувач у нас один, а пристроїв у нього може бути кілька, також одним пристроєм можуть користуватися кілька користувачів;
  • Відправка повідомлень користувачам не найдешевша операція і тому подію, ініційоване відправлення повідомлення, потрібно ставити в чергу на відправку;
  • Тільки маленькі проєкти з малим числом одержувачів можуть дозволити собі відправляти повідомлення по події, протягом того ж HTTP запиту;
  • Так у нас з'являється система черг на RabbitMQ, Redis і т. д.;
  • З'являються демони/воркери які розбирають чергу і інші інструменти підтримки черг;
  • Для збільшення швидкості завантаження можна розпаралелити процес і рознести його на кілька нод.

Практика

Нарешті ми перейшли до найголовнішого. Як я вже говорив раніше, в якості сервера повідомлень ми будемо використовувати Firebase Cloud Messaging, тому ми починаємо з реєстрації і створення проекту на Firebase.

Тут все просто:

  • Заходимо на сайт;
  • Реєструємося;
  • Тиснемо кнопку Create new project або Import Google project, якщо у вас вже є проєкт;
  • При створенні вказуємо назву проєкту та країну;
  • Після створення проекту потрапляємо на його dashboard;
  • У меню наводимо на коліщатко поруч з Огляд і вибираємо Project settings;
  • На сторінці переходимо у вкладку Cloud Messaging;
  • Нас цікавить key Server, який буде використовуватися для відправки повідомлень з сервера і Sender ID, який буде використовуватися для отримання повідомлень на стороні клієнта.

Можна ще покопатися в налаштуваннях і погратися з розподілом прав доступу, але, загалом-то, робота з сайтом Firebase закінчена.

Приступаємо до написання клієнта

Почнемо з того, що створимо Service Worker для отримання push-повідомлень.
Створюємо файл firebase-messaging-sw.js з наступним вмістом.

// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/3.6.8/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.6.8/firebase-messaging.js');

firebase.initializeApp({
    messagingSenderId: ''
});

const messaging = firebase.messaging();

де

  • — це Sender ID, який ми отримали після реєстрації в Firebase.
Важливе зауваження

Файл Service Worker-а повинен називатися саме firebase-messaging-sw.js і обов'язково повинен знаходитися в корені проекту, тобто доступний за адресою https://example.com/firebase-messaging-sw.js. Шлях до цього файлу жорстко прописаний в бібліотеці Firebase.

Написаного коду достатньо для того, щоб показувати попередження. Про додаткові можливості поговоримо трохи пізніше. Тепер додамо бібліотеку Firebase і скрипт підписки у наш шаблон сторінки.

 

Додаємо на сторінку кнопку для підписки на повідомлення

Следить за изменениями

Підписка на повідомлення

// firebase_subscribe.js
firebase.initializeApp({
    messagingSenderId: ''
});

// браузер підтримує повідомлення
// взагалі, цю перевірку повинна робити бібліотека Firebase, але вона цього не робить
if ('Notification' in window) {
    var messaging = firebase.messaging();

    // користувач вже дозволив отримання повідомлень
    // підписуємо на повідомлення якщо ще не підписали
    if (Notification.permission === 'granted') {
        subscribe();
    }

    // по кліку, запитуємо у користувача дозвіл на повідомлення
    // і підписуємо його
    $('#subscribe').on('click', function () {
        subscribe();
    });
}

function subscribe() {
    // запитуємо дозвіл на отримання повідомлень
    messaging.requestPermission()
        .then(function () {
            // отримуємо ID пристрою
            messaging.getToken()
                .then(function (currentToken) {
                    console.log(currentToken);

                    if (currentToken) {
                        sendTokenToServer(currentToken);
                    } else {
                        console.warn('Не вдалося отримати токен.');
                        setTokenSentToServer(false);
                    }
                })
                .catch(function (err) {
                    console.warn('При отриманні сертифіката сталася помилка.', err);
                    setTokenSentToServer(false);
                });
    })
    .catch(function (err) {
        console.warn('Не вдалося отримати дозвіл на показ повідомлень.', err);
    });
}

// відправлення ID на сервер
function sendTokenToServer(currentToken) {
    if (!isTokenSentToServer(currentToken)) {
        console.log('Відправка токена на сервер...');

        var url = ''; // адреса скрипта на сервері який зберігає ID пристрою
        $.post(url, {
            token: currentToken
        });

        setTokenSentToServer(currentToken);
    } else {
        console.log('Токен вже відправлений на сервер.');
    }
}

// використовуємо localStorage для оцінки того,
// що користувач підписався на повідомлення
function isTokenSentToServer(currentToken) {
    return window.localStorage.getItem('sentFirebaseMessagingToken') == currentToken;
}

function setTokenSentToServer(currentToken) {
    window.localStorage.setItem(
        'sentFirebaseMessagingToken',
        currentToken ? currentToken : ''
    );
}

Ось і все. Це весь код, який потрібен для отримання push-повідомлень.

Відправка повідомлень з сервера

У загальному вигляді відправлення повідомлення виглядає так:

POST /fcm/send HTTP/1.1
Host: fcm.googleapis.com
Authorization: key=YOUR-SERVER-KEY
Content-Type: application/json

{
  "notification": {
    "title": "Заголовок повідомлення",
    "body": "Текст повідомлення",
    "icon": "https://example.com/logo.png",
    "click_action": "http://example.com/"
  },
  "to": "YOUR-TOKEN-ID"
}

де

  • YOUR-SERVER-KEY — це key Server, який ми отримали при реєстрації в Firebase;
  • YOUR-TOKEN-ID — це ID пристрою конкретного користувача.

Всі поля по порядку:

  • notification — параметри повідомлення;
  • title — заголовок повідомлення. Ліміт 30 символів;
  • body — текст повідомлення. Ліміт 120 символів;
  • icon — іконка повідомлення. Є деякі стандарти розмірів іконок, але я використовую 192x192. Іконки меншого розміру погано виглядають на мобільних пристроях;
  • click_action URL-адреса сторінки, на яку перейде користувач клікнувши по повідомленню;
  • to — ID пристрою одержувача повідомлення;
  • Повний список параметрів тут.

Web PUSH Notifications швидко і простоЦе приклад відправки одного повідомлення одному одержувачу. Можна відправити одне повідомлення відразу декільком одержувачам. Аж до 1000 одержувачів за раз.

{
  "notification": {
    "title": "Заголовок повідомлення",
    "body": "Текст повідомлення",
    "icon": "https://example.com/logo.png",
    "click_action": "http://example.com/"
  },
  "registration_ids": [
    "YOUR-TOKEN-ID-1",
    "YOUR-TOKEN-ID-2"
    "YOUR-TOKEN-ID-3"
  ]
}

Приклад відповідей від сервера повідомлень:

Надсилання повідомлення в Chrome

{
    "multicast_id": 6407277574671070000,
    "success": 1,
    "failure": 0,
    "canonical_ids": 0,
    "results": [
        {
            "message_id": "0:1489072146895227%e609af1cf9fd7ecd"
        }
    ]
}

Надсилання повідомлення в FireFox

{
    "multicast_id": 7867877497742898000,
    "success": 1,
    "failure": 0,
    "canonical_ids": 0,
    "results": [
        {
            "message_id": "https://updates.push.services.mozilla.com/m/gAAAAABYwWmlTCKje5OLwedhNUQr9LbOCmZ0evAF9HJBnR-v7DF2KEkZY3zsT8AbrqB6JfJO6Z6vsotLJMmiIvJs9Pt1Q9oc980BRX2IU1-jlzRLIhSVVBLo2i80kBvTMYadVAMIlSIyFkWm-qg_DfLbenlO9z1S4TGMJl0XbN5gKMUlfaIjnX2FBG4XsQjDKasiw8-1L38v"
        }
    ]
}

Помилка відправки повідомлення

{
    "multicast_id": 8165639692561075000,
    "success": 0,
    "failure": 1,
    "canonical_ids": 0,
    "results": [
        {
            "error": "InvalidRegistration"
        }
    ]
}

Повний список кодів помилок.

Ми не прив'язані до якоїсь конкретної мови програмування і для простоти прикладу будемо використовувати PHP з розширенням cURL. Скрипт відправки повідомлення потрібно запускати з консолі.

#!/usr/bin/env php
 $YOUR_TOKEN_ID,
    'notification' => [
        'title' => 'Заголовок',
        'body' => 'Текст повідомлення',
        'icon' => 'https://example.com/logo.png',
        'click_action' => 'http://example.com/',
    ],
];
$fields = json_encode($request_body);

$request_headers = [
    'Content-Type: application/json',
    'Authorization: key=' . $YOUR_API_KEY,
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

echo $response;

messaging.onMessage

Обробник messaging.onMessage вартий окремої згадки, так як він відноситься як раз до категорії підводних каменів. У прикладах від Firebase я не бачив прикладу використання цього обробника. 

Що ж це за обробник і як він працює. З документації ми знаємо, що цей обробник викликається якщо ми отримуємо push-повідомлення і знаходимося в цей момент на сторінці сайту з якого відправлено повідомлення (бажаючі використовувати нативне рішення можуть подивитися приклад реалізації). Ця функція дуже корисна тим, що ми можемо відобразити повідомлення на сторінці зробивши красиве модальне вікно або ще щось. У мене такої необхідності немає, тому я просто виведу стандартне повідомлення.

if ('Notification' in window) {
    var messaging = firebase.messaging();

    messaging.onMessage(function(payload) {
        console.log('Message received. ', payload);
        new Notification(payload.notification.title, payload.notification);
    });

    // ...
}

Ніби все просто, але є підводний камінь. Справа в тому, що на мобільних пристроях заборонено використовувати конструктор Notification. І для вирішення цієї проблеми потрібно використовувати ServiceWorkerRegistration.showNotification() і обробник у цьому випадку буде мати вигляд:

messaging.onMessage(function(payload) {
    console.log('Message received. ', payload);

    // реєструємо порожній ServiceWorker кожен раз
    navigator.serviceWorker.register('messaging-sw.js');

    // запитуємо права на показ повідомлень якщо ще не отримали їх
    Notification.requestPermission(function(result) {
        if (result === 'granted') {
            navigator.serviceWorker.ready.then(function(registration) {
                // тепер ми можемо показати повідомлення
                return registration.showNotification(payload.notification.title, payload.notification);
            }).catch(function(error) {
                console.log('ServiceWorker registration failed', error);
            });
        }
    });
});

Тепер повідомлення працюють і на мобільних пристроях. Здавалося б вже все, але ні. Не дивлячись на запевнення деяких, ServiceWorker не повинен бути порожнім. Ми ж хочемо, що б по кліку користувач переходив на потрібну нам сторінку. Для цього нам потрібно додати обробник кліка по повідомленню в ServiceWorker.

Зберігаємо параметри повідомлення для доступу властивості click_action в ServiceWorker.

// ...
navigator.serviceWorker.ready.then(function(registration) {
    payload.notification.data = payload.notification; // параметри повідомлення
    registration.showNotification(payload.notification.title, payload.notification);
}).catch(function(error) {
    console.log('ServiceWorker registration failed', error);
});
// ...

Обробляємо клік по повідомленню в ServiceWorker.

// messaging-sw.js
self.addEventListener('notificationclick', function(event) {
    const target = event.notification.data.click_action || '/';
    event.notification.close();

    // цей код повинен перевіряти список відкритих вкладок і переключаться на відкриту
    // вкладку з посиланням якщо така є, інакше відкриває нову вкладку
    event.waitUntil(clients.matchAll({
        type: 'window',
        includeUncontrolled: true
    }).then(function(clientList) {
        // clientList почему-то всегда пуст!?
        for (var i = 0; i < clientList.length; i++) {
            var client = clientList[i];
            if (client.url == target && 'focus' in client) {
                return client.focus();
            }
        }

        // Відкриваємо нове вікно
        return clients.openWindow(target);
    }));
});

TTL і додатковий контроль над повідомленням

Важливою властивістю для повідомлення є час його актуальності. Це залежить від ваших бізнес-процесів. За замовчуванням час життя повідомлень 4 тижні. Це дуже багато для повідомлень такого характеру. Наприклад, повідомлення "Ваша улюблена передача починається через 15 хвилин" актуально протягом 15 хвилин. Після цього повідомлення вже не актуально і показуватися не повинно. За контроль над часом життя відповідає властивість time_to_live із значенням від 0 до 2419200 секунд. Читати детальніше документації. Повідомлення з вказаним TTL буде мати вигляд:

{
  "notification": {
    "title": "Заголовок повідомлення",
    "body": "Текст повідомлення",
    "icon": "https://example.com/logo.png",
    "click_action": "http://example.com/"
  },
  "time_to_live": 900,
  "to": "YOUR-TOKEN-ID"
}

Повідомлення виду "Ваша улюблена передача починається через 15 хвилин" актуально протягом 15 хвилин, але вже через хвилину після відправки воно стане не коректним. Тому що передача почнеться не через 15 хвилин, а вже через 14. Контролювати такі ситуації потрібно на стороні клієнта.

Висновок

А тепер поговоримо про сумне. Не дивлячись на всі принади технології, у неї є ряд недоліків:

  1. Найголовніша проблема це, як завжди, підтримка в браузерах. Повноцінна підтримка є в Chrome, Firefox і Opera останніх версій. IE, Safari, Opera Mini, UC Browser, Dolphin та інша братія залишаються за бортом. Але зате працює в мобільних версіях браузерів Chrome, Firefox і Opera.
  2. Відкритий сайт і працючий Service Worker не гарантують доставку повідомлення. Хоча повідомлення можуть дійти і при закритому браузері.

Бібліотека Firebase приховує в собі багато таємниць і її дослідження могло б дати відповіді на деякі питання, але це вже виходить за рамки цієї статті.

Посилання

Спиок посилань по темі

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація