Web PUSH Notifications быстро и просто
- вторник, 14 марта 2017 г. в 03:14:31
Добрый день. В этой небольшой заметке я хочу рассказать как быстро и просто настроить push-уведомления на вашем сайте. Эта статья ни в коем случае не претендует на звание исчерпывающего руководства, но, я надеюсь, что она даст точку старта для дальнейшего изучения.
Информации по этой теме в интернете полно, но она фрагментирована, разбросана по разным ресурсам и перемешена с уведомлениями для мобильных устройств с примерами на Java, C++ и Python. Нас же, как веб-разработчиков, интересует JavaScript. В этой статье я постараюсь саккумулировать всю необходимую и полезную информацию.
Я думаю, вы уже знаете что такое push-уведомления, но я всё же напишу коротко о главном.
Пользователь, заходя на сайт, вытягивает (pull) с него данные. Это удобно и безопасно, но с развитием интернет ресурсов, появилась необходимость оперативно доставлять информацию пользователям не дожидаясь пока те сами сделают запрос. Так и появилась технология принудительной доставки (push) данных с сервера клиенту.
Важно
Push-уведомления работают только если у вас на сайте есть HTTPS.
Без валидного SSL сертификата запустить не получится. Так что если у вас еще нет поддержки HTTPS, то пришло время её сделать. Рекомендую воспользоваться Let's Encrypt.
Для запуска на localhost нужно прибегать к хитростям. Я же тестировал скрипты на Github Pages.
Сразу хочу оговориться, что push-уведомления не для рекламных рассылок. Отправлять нужно только то, что действительно нужно конкретному пользователю и на что он действительно должен оперативно отреагировать.
Хороший пример:
Плохой пример:
Плохие примеры тоже требуют уведомления, но на них не нужно реагировать оперативно. Эти уведомления можно отправить на почту. Вообще, все важные уведомления рекомендуется дублировать на почту, так-как push-уведомления могут не дойти до пользователя по разным, не зависящих от вас, причинам. Также важным фактором является актуальность события. Об этом я еще поговорю чуть позже. Рекомендую к прочтению:
Вернемся к нашим баранам. Так как же всё это работает? Для начала немного теории.
Среди непосвященных бытует мнение что push-уведомления это простая технология, не требующая для реализации особых ресурсов. В действительности же это целый пул технологий.
Для начала небольшая схема того как все это работает (анимированная схема):
К сожалению, мне не удалось выяснить кто и как создает ID устройства и как сервер сообщений привязывается к конкретному устройству. Я использовал сервер сообщений Firebase Cloud Messaging от Google и его библиотеку. К сожалению, я не смог выяснить можно ли его заменить на свой сервер и как это сделать.
Забавный факт
Изначально для отправки сообщений использовали:
Cloud to Device Messaging
Потом его заменили на:
Google Cloud Messaging
А потом еще раз поменяли на:
Firebase Cloud Messaging
Интересно, что дальше.
Заметка
Google рекомендует использовать переключатель для подписки и отписки от уведомлений. Таким образом, инициация процедуры подписки на уведомления исходит от пользователя, а не от сайта. Принудительно подписывать на уведомления каждого приходящего пользователя, это плохая практика.
Это все выглядит очень сложно, но на сервере все не проще.
Наконец-то, мы перешли к самому главному. Как я уже говорил ранее, в качестве сервера сообщений мы будем использовать Firebase Cloud Messaging, поэтому мы начинаем с регистрации и создания проекта на Firebase.
Тут все просто:
Можно еще покопаться в настройках и поиграться с разделением прав доступа, но, в общем-то, работа с сайтом 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: '<SENDER_ID>'
});
const messaging = firebase.messaging();
где,
<SENDER_ID>
— это Sender ID который мы получили после регистрации в Firebase.Важное замечание
Файл Service Worker-а должен называться именно firebase-messaging-sw.js и обязательно должен находиться в корне проекта, то есть доступен по адресу https://example.com/firebase-messaging-sw.js. Путь к этому файлу жестко прописан в библиотеке Firebase.
Написанного кода достаточно для того чтобы показывать уведомления. О дополнительных возможностях поговорим чуть позже. Теперь добавим библиотеку Firebase и скрипт подписки в наш шаблон страницы.
<script type="text/javascript" src="//www.gstatic.com/firebasejs/3.6.8/firebase.js"></script>
<script type="text/javascript" src="/firebase_subscribe.js"></script>
Добавляем на страницу кнопку для подписки на уведомления
<button type="button" id="subscribe">Следить за изменениями</button>
Подписка на уведомления
// firebase_subscribe.js
firebase.initializeApp({
messagingSenderId: '<SENDER_ID>'
});
// браузер поддерживает уведомления
// вообще, эту проверку должна делать библиотека 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": "Начало в 21:00",
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=40&height=40",
"click_action": "http://eralash.ru/"
},
"to": "YOUR-TOKEN-ID"
}
где,
YOUR-SERVER-KEY
— это Server key который мы получили при регистрации в Firebase;YOUR-TOKEN-ID
— это ID устройства конкретного пользователя.Все поля по порядку:
notification
— параметры уведомления;title
— заголовок уведомления. Лимит 30 символов;body
— текст уведомление. Лимит 120 символов;icon
— иконка уведомления. Есть некоторые стандарты размеров иконок, но я использую 192x192. Иконки меньшего размера плохо смотрятся на мобильных устройствах;click_action
— URL адрес страницы на которую перейдет пользователь кликнув по уведомлению;to
— ID устройства получателя уведомления;Это пример отправки одного уведомления одному получателю. Можно отправить одно уведомление сразу нескольким получателям. Вплоть до 1000 получателей за раз.
{
"notification": {
"title": "Ералаш",
"body": "Начало в 21:00",
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192",
"click_action": "http://eralash.ru/"
},
"registration_ids": [
"YOUR-TOKEN-ID-1",
"YOUR-TOKEN-ID-2"
"YOUR-TOKEN-ID-3"
]
}
Пример ответов от сервера сообщений:
{
"multicast_id": 6407277574671070000,
"success": 1,
"failure": 0,
"canonical_ids": 0,
"results": [
{
"message_id": "0:1489072146895227%e609af1cf9fd7ecd"
}
]
}
{
"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
<?php
$url = 'https://fcm.googleapis.com/fcm/send';
$YOUR_API_KEY = ''; // Server key
$YOUR_TOKEN_ID = ''; // Client token id
$request_body = [
'to' => $YOUR_TOKEN_ID,
'notification' => [
'title' => 'Ералаш',
'body' => sprintf('Начало в %s.', date('H:i')),
'icon' => 'https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192',
'click_action' => 'http://eralash.ru/',
],
];
$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;
Важным свойством для уведомления может является время его актуальности. Это зависит от ваших бизнес процессов. По умолчанию время жизни уведомлений 4 недели. Это очень много для уведомлений такого характера. Например, уведомление "Ваша любимая передача начинается через 15 минут" актуально в течении 15 минут. После этого сообщение уже не актуально и показываться не должно. За контроль над временем жизни отвечает свойство time_to_live
со значением от 0 до 2419200 секунд. Подробней читать в документации. Сообщение с указанным TTL будет иметь вид:
{
"notification": {
"title": "Ералаш",
"body": "Начало через 15 минут",
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192",
"click_action": "http://eralash.ru/"
},
"time_to_live": 900,
"to": "YOUR-TOKEN-ID"
}
Сообщение вида "Ваша любимая передача начинается через 15 минут" актуально в течении 15 минут, но уже через минуту после отправки оно станет не корректным. Потому что передача начнется не через 15 минут, а уже через 14. Контролировать такие ситуации нужно на стороне клиента.
Для этого мы поменяем отправляемое с сервера сообщение:
{
"data": {
"title": "Ералаш",
"time": 1489006800,
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192",
"click_action": "http://eralash.ru/"
},
"time_to_live": 900,
"to": "YOUR-TOKEN-ID"
}
Обратите внимание что поле notification
поменялось на data
. Теперь не будет вызываться обработчик по умолчанию Firebase и нам нужно самостоятельно сделать это. Добавим в конце файла firebase-messaging-sw.js
следующие строки:
// регистрируем свой обработчик уведомлений
messaging.setBackgroundMessageHandler(function(payload) {
if (typeof payload.data.time != 'undefined') {
var time = new Date(payload.data.time * 1000);
var now = new Date();
if (time < now) { // истек срок годности уведомления
return null;
}
var diff = Math.round((time.getTime() - now.getTime()) / 1000);
// показываем реальное время в уведомлении
// будет сгенерировано сообщение вида: "Начало через 14 минут, в 21:00"
payload.data.body = 'Начало через ' +
Math.round(diff / 60) + ' минут, в ' + time.getHours() + ':' +
(time.getMinutes() > 9 ? time.getMinutes() : '0' + time.getMinutes())
;
}
// Сохраяем data для получения пареметров в обработчике клика
payload.data.data = payload.data;
// Показываем уведомление
return self.registration.showNotification(payload.data.title, payload.data);
});
// свой обработчик клика по уведомлению
self.addEventListener('notificationclick', function(event) {
// извлекаем адрес перехода из параметров уведомления
var target = event.notification.data.click_action || '/';
event.notification.close();
// этот код должен проверять список открытых вкладок и переключатся на открытую
// вкладку с ссылкой если такая есть, иначе открывает новую вкладку
event.waitUntil(clients.matchAll({
type: 'window'
}).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();
}
}
// Открываем новое окно
if (clients.openWindow) {
return clients.openWindow(target);
}
}));
});
Вот таким незамысловатым образом мы получили полный контроль над уведомлением. Что самое интересное, пользователю мы показываем время уведомления в его часовом поясе. Это актуально для сервисов который работают по всему миру или регионах с широким разбросом часовых поясов как у матушки-России.
А теперь поговорим о грустном. Не смотря на все прелести технологии, у неё есть ряд недостатков:
Библиотека Firebase скрывает в себе много тайн и её исследование могло бы дать ответы на некоторые вопросы, но это уже выходит за рамки этой статьи.
Так как для запуска Service Worker-а нужен HTTPS, то самым простым решением было разместить проект на GitHub Pages, что я и сделал.
Проект доступен по адресу: https://github.com/peter-gribanov/serviceworker
Исходники проекта: https://peter-gribanov.github.io/serviceworker/
Проект представляет из себя только клиентскау часть. Для того что бы получить уведомление надо:
Далее, в любом инструменте отправки HTTP запросов отправляем, уже описанный ранее, запрос. Можно использовать сURL, я предпочитаю приложение Postman для Chrome.
POST /fcm/send HTTP/1.1
Host: fcm.googleapis.com
Authorization: key=AAAAaGQ_q2M:APA91bGCEOduj8HM6gP24w2LEnesqM2zkL_qx2PJUSBjjeGSdJhCrDoJf_WbT7wpQZrynHlESAoZ1VHX9Nro6W_tqpJ3Aw-A292SVe_4Ho7tJQCQxSezDCoJsnqXjoaouMYIwr34vZTs
Content-Type: application/json
{
"notification": {
"title": "Ералаш",
"body": "Начало в 21:00",
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192",
"click_action": "http://eralash.ru/"
},
"to": "YOUR-TOKEN-ID"
}
где,
YOUR-TOKEN-ID
— это ID устройства который вы получили на странице приложения.Вот и все. Получаем уведомление и радуемся жизни.
Для статьи, я хотел сделать страницу на которой была бы кнопка, нажав на которую получаешь push-уведомление, но из этого ничего не вышло. Ни через AJAX, ни через веб-формы уведомления не доходят. Возможно оно и к лучшему.
Можете убедится в этом сами.
Проект доступен по адресу: https://push.peter-gribanov.ru/
Исходники проекта: https://github.com/peter-gribanov/article-push
Релизы проекта: https://github.com/peter-gribanov/article-push/releases
Пример сообщения для отправки через HTTP клиенты:
POST /fcm/send HTTP/1.1
Host: fcm.googleapis.com
Authorization: key=AAAAaGQ_q2M:APA91bGCEOduj8HM6gP24w2LEnesqM2zkL_qx2PJUSBjjeGSdJhCrDoJf_WbT7wpQZrynHlESAoZ1VHX9Nro6W_tqpJ3Aw-A292SVe_4Ho7tJQCQxSezDCoJsnqXjoaouMYIwr34vZTs
Content-Type: application/json
{
"data": {
"title": "Ералаш",
"body": "Начало в 21:00",
"icon": "https://eralash.ru.rsz.io/sites/all/themes/eralash_v5/logo.png?width=192&height=192",
"click_action": "http://eralash.ru/"
},
"to": "YOUR-TOKEN-ID"
}
где,
YOUR-TOKEN-ID
— это ID устройства который вы получите после подтверждения. ID устройства будет отличатся от того что на GitHub Pages и показывается он только в консоли браузера.Обратите внимание, на GitHub Pages используется свойство notification
, а на моей площадке data
.