Добрый день. В этой небольшой заметке я хочу рассказать как быстро и просто настроить 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;
  • В меню наводим на колесико рядом с Overview и выбираем Project settings;
  • На открывшейся странице переходим во вкладку Cloud Messaging;
  • Нас интересует Server key, который будет использоваться для отправки сообщений с сервера и 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: '<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"
  ]
}

Пример ответов от сервера сообщений:


Отправка уведомления в 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
<?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;

messaging.onMessage


Обработчик messaging.onMessage стоит отдельного упоминания, так как он относится как раз к категории подводных камней. В примерах от Firebase я не видел примера использование этого обработчика. О нем мне рассказал FluorescentHallucinogen, за что ему отдельное спасибо, но он не упомянул о некоторых особенностях его использования.


Что же это за обработчик и как он работает. Из документации мы знаем, что этот обработчик вызывается если мы получаем 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": "Начало через 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) {
    // извлекаем адрес перехода из параметров уведомления 
    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);
    }));
});

Вот таким незамысловатым образом мы получили полный контроль над уведомлением. Что самое интересное, пользователю мы показываем время уведомления в его часовом поясе. Это актуально для сервисов который работают по всему миру или регионах с широким разбросом часовых поясов как у матушки-России.


Заключение


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


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

Библиотека Firebase скрывает в себе много тайн и её исследование могло бы дать ответы на некоторые вопросы, но это уже выходит за рамки этой статьи.


Поиграться


Проект на GitHub Pages


Так как для запуска Service Worker-а нужен HTTPS, то самым простым решением было разместить проект на GitHub Pages, что я и сделал.


Проект доступен по адресу: https://github.com/peter-gribanov/serviceworker
Исходники проекта: https://peter-gribanov.github.io/serviceworker/



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


  • Зайти на страницу;
  • Нажать кнопку Register;
  • Браузер запросит разрешение на показ уведомлений;
  • Подтверждаем разрешение;
  • На странице и в консоли браузера будет напечатан ID вашего устройства;
  • Появится кнопка Delete Token для удаления существующего токена и повторной регистрации;
  • Появится форма с параметрами уведомления которое можно отправить нажав на кнопку Send;
  • Меняем параметры по усмотрению и получаем разные уведомления.

Можно отправить уведомление через любой инструмент для отправки 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 устройства который вы получили на странице приложения.

Вот и все. Получаем уведомление и радуемся жизни.


Ссылки


Поделиться с друзьями
-->

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


  1. S_A
    13.03.2017 08:32

    Прямо стало интересно, почему не доходят. Посмотрел весь код, наскоком ничего не обнаружил, надо повторять всё разбираться.

    У меня сомнения что всё это не работает, потому что в случае с OneSignal это как-то отрабатывает. Банально ставишь плагин уведомлений к Wordpress, жмёшь «Send test notification» и оно приходит.

    Как гипотеза, js api от firebase выдает не тот токен (он может и клиентский, да не тот).


    1. BupycNet
      13.03.2017 08:56

      У Firefox вообще свой пуш- сервис. Firebase проксирует через себя запрос и бага где то на стороне этого проксирования. Мы шлем напрямую в Mozilla Push Service и все приходит. При этом не используем payload, а запрашиваем уведомления с сервера, что позволяет еще в дальнейшем показывать лишь те, что пользователь не видел на других платформах.


      1. ghost404
        13.03.2017 09:29
        +1

        Интересно было бы услышать более детально как вы всё реализовали у себя в PushAll


    1. ghost404
      16.03.2017 13:16
      +1

      Уведомления не доходили потому что нужно было еще прописать messaging.onMessage().


      Сейчас все поправил и сделал полноценное приложение с отправкой уведомлений через js


  1. Sabbaot
    13.03.2017 09:15
    +8

    ужас как бесят эти пуш-уведомления. ощущение, что их специально кто-то проталкивает. в гугл хроме при включенной галочке на «отказаться от пуш-уведомлений», пуш сообщения отдельных гениальных сайтов красавцев все равно проходят (вести ру к примеру). на других сайтах приходится при каждом заходе на страничку отказываться, отказываться и отказываться от подписок на сайт. ff пока держит защиту с dom.push.enabled false
    вы не думайте, я не против прогресса. но ситуация как с рекламой — когда ею начали злоупотреблять, то нашлись всеми рекламодателями нелюбимые контрмеры. я бы даже как-нибудь сел, да и запилил пост-фидбек по всему этому новому лицу интернета, что раньше было удобнее, а что сейчас.


    1. bitver
      13.03.2017 10:19
      +1

      А всё потому что технология очень удобна если использовать по назначению, но народ не знает о ней, народу надо показать что да как. Аналогия с мелкомягким когда обучали пользоваться мышью.

      Ещё, кажется, так они(Гугл, в основном) стараются разрабов Apple(см. iOS, Service Workers) подтолкнуть внедрить push у себя в браузере и чему я рад, так как не хватает сильно в работе, связанной с темой умных домов.


    1. Busla
      13.03.2017 11:09

      та же история, что и с открытием ссылок в новом окне: разумное в отдельных случаях поведение стали массово использовать для накрутки пользователей


    1. megahertz
      13.03.2017 14:23

      Полагаю, что часто это выглядит так. Заказчик, далекий от IT открывает для себя PUSH и сразу озвучивает свою хотелку. На все уверения о том что многих будет бесить, что нужно запрашивать разрешение только по клику отвечает: "Хочу чтобы работало сразу при заходе на сайт".


  1. mixaly4
    13.03.2017 10:15
    +1

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

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


    1. ghost404
      13.03.2017 10:54

      Видел и задавался тем же вопросом. В документации не описаны какие либо методы по кастомизации окна. Подозреваю что это делается через какие-то костыли и хуки. Не советую вам это делать. Единообразие интерфейса не есть плохо


      1. dmitryrublev
        13.03.2017 17:32

        Это кастомное окно после клика по нему откроет стандартное браузерное, скорее всего.


  1. dadyjo
    13.03.2017 11:26
    +1

    Не знаю кого как, но меня жутко раздражают эти всплывающие «Блабла сайт запрашивает разрешение на: Показывать оповещение» Разрешить Блокировать.


    1. random1st
      13.03.2017 11:58

      Их можно отключить. Что касается push уведомлений, вполне себе неплохо работает OneSignal. Правда, есть одна подковырка — они прямо говорят, что анализируют их содержание. Однако с ними можно договориться о платной подписке. К слову, тарифы на платную подписку у них существенно ниже, чем у конкурентов.


  1. iSage
    13.03.2017 12:12

    1. Если использовать push api напрямую, а не через firebase.js — то все замечательно приходит в фаерфокс. Кода от этого больше не становится.
    2. Если использовать push api напрямую, то VAPID и надо шифровать payload.
    3. >не самая дешевая операция
    Да, если делать это (multi)curl'ом на похопе (~250 пушей в минуту). На golang с concurency=200 (столько хочет гугловый http2) получается ~1000 пушей в секунду.
    4. Стандарт до сих пор не утвержден. И лучше не изобретать свой велосипед, а использовать один из кучи готовых сервисов, которые и о передрягах стандарта позаботятся и поддержку iOS/Safari обеспечат.


    1. iSage
      13.03.2017 12:26

      Ах да, https нужен только для service-worker. Отправку можно делать откуда угодно. При этом картинки для нотификаций тоже должны отдаваться с https.


      1. ghost404
        13.03.2017 13:43

        Отправлять можно откуда угодно, а вот получать уведомления на клиенте без Service Worker нельзя. Уведомления без Service Worker уже не push-уведомления. Это неразделимое целое. Потому я и говорю в статье что push-уведомления это не одна технология, а целый набор.


        1. iSage
          13.03.2017 13:45
          -1

          Вот ты опять отвечаешь на то, о чем я не говорил.


    1. ghost404
      13.03.2017 13:34

      3. не самая дешевая операция
      Да, если делать это (multi)curl'ом на похопе (~250 пушей в минуту). На golang с concurency=200 (столько хочет гугловый http2) получается ~1000 пушей в секунду.

      окей. А теперь представим что у вас 5000 подписчиков. Это значит что страница у вас будет открываться на 5 секунд дольше обычного и пользователи будут отказываться от вашего ресурса из-за ожидания, не смотря на то что вы используете супер продвинутый Go. Поставить же событие в очередь займет милисикунды, а разбирать очередь можете на чем угодно в фоне, хоть на PHP, хоть на Go, хоть на C.


      4. Стандарт до сих пор не утвержден. И лучше не изобретать свой велосипед, а использовать один из кучи готовых сервисов, которые и о передрягах стандарта позаботятся и поддержку iOS/Safari обеспечат.

      А кто изобретает велосипед? Как раз напротив. Я пытаюсь показать как реализовать уведомления используя стандартные средства (в данном случае средствами представленными Google), не прибегая к сторонним сервисам. Сторонние сервисы плохи тем что:


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

      Внимание вопрос. Зачем платить за то что можно сделать бесплатно? Причем абсолютно легально и стандартизировано.


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


      PS: я уже обсуждал тему осмысленности сторонних сервисов для отправки push-уведомлений с автором проекта PushAll BupycNet и не хотел бы возвращаться к этой теме.


      1. iSage
        13.03.2017 13:44

        1. Заметь, я же нигде не говорил, что очередь не нужна. Но даже в фоне разбирать события курлом — плохой вариант.
        2. firebase api — это не стандартное средство. Стандартное — webpush api. Гугл его целиком и полностью поддерживает.


        1. ghost404
          13.03.2017 14:01

          1. Заметь, я же нигде не говорил, что очередь не нужна. Но даже в фоне разбирать события курлом — плохой вариант.

          Надо было так и говорит. "Не используйте cURL." Я вас неправильно понял.


          2. firebase api — это не стандартное средство. Стандартное — webpush api.

          Хорошо. Назовем это так — Библиотека/обертка над нативным интерфейсом. Что в этом плохого?
          Согласен, лучше это решать нативными средствами.
          Может вы поделитесь с обществом своим опытом в этой сфере?


          1. iSage
            13.03.2017 14:08

            1. Мы с тобой несколько лет вместе работали, можно и на «ты», хех
            2. У Мозиллы есть несколько довольно подробных и понятных заметок с примерами, они работают и для хрома (с добавлением FCM-ключа и манифеста) https://developer.mozilla.org/en/docs/Web/API/Push_API#See_also
            Для хрома стоит прочесть https://developers.google.com/web/updates/2015/03/push-notifications-on-the-open-web (не смотря на пометку об устаревании) и минимально-рабочий пример https://github.com/chrisdavidmills/push-api-demo


        1. ghost404
          13.03.2017 15:18

          1. Заметь, я же нигде не говорил, что очередь не нужна. Но даже в фоне разбирать события курлом — плохой вариант.

          Раз пошла такая тема. Нужно определится на чем написан сайт и бизнес логика проекта. Бизнес логика это самое важное в проекте и она определяет как отреагировать на то или иное событие.


          Если проект написан на Go, то логично и уведомления отправлять через Go.


          Если проект написан на PHP (как это часто бывает), то можно завести вторую очередь. Первая хранит события, а вторая уже готовые уведомления которые нужно отправить. А вот в качестве транспорта для отправки уведомлений из второй очереди, можно в этом случае использовать микросервисы на Go.


          В этом случае мы можем потерять в производительности из-за 2 очередей, но зато логика будет прозрачной.


          1. iSage
            13.03.2017 15:22

            У нас сейчас проект просто отсылает сообщение в апишечку на похопе, которая кладет в очередь. А сервис на го уже берет из очереди и рассылает.


            1. FluorescentHallucinogen
              14.03.2017 22:51

              Если использовать Firebase Cloud Messaging, то очередь не нужна: https://firebase.google.com/docs/cloud-messaging/js/send-multiple.


    1. FluorescentHallucinogen
      14.03.2017 22:26

      Стандарт до сих пор не утвержден. И лучше не изобретать свой велосипед, а использовать один из кучи готовых сервисов, которые и о передрягах стандарта позаботятся и поддержку iOS/Safari обеспечат.

      Да, спецификация W3C Push API не утверждена в том смысле, что ещё пока не достигла статуса рекомендации W3C, но её уже можно активно использовать. Куда более важна поддержка спецификации различными браузерами: http://caniuse.com/#feat=push-api. Более детальные сведения: https://github.com/web-push-libs/web-push#browser-support. Хотя эти сведения тоже неполные — нет деления на платформы, что важно, так как, например, на iOS все браузеры вынуждены использовать движок WebKit со всеми вытекающими из этого последствиями в виде поддержки спецификаций.


      Многие спецификации, например Service Workers и Web Components до сих пор не получили статус рекомендации W3C, но это не повод их не использовать.


  1. Evengard
    13.03.2017 12:32
    +2

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


    1. iSage
      13.03.2017 12:45
      +2

      Краткий ответ: никак
      Длинный ответ: ServiceWorker+ Fetch/Streams. И свой костыль для fingerprinting'а клиента, подписки, отписки и всего вот этого. Сделать свой, совместимый с WebPush API сервер не представляется возможным, поскольку их адреса вшиты в коде браузеров и не доступны к изменению из js.


      1. Evengard
        13.03.2017 14:32

        Кхм, мне это кажется странным… Браузер, зависящий от внешнего сервера? Получается что все сайты, желающие воспользоваться этим ОТКРЫТЫМ API, должны тоже зависеть от этих серверов? Либо костылять что-то своё нестандартное? Что-то тут неправильно.


        1. iSage
          13.03.2017 14:39

          Ну, в гугле, например, это в том числе ради более точной идентификации клиента (привязка к гугловой учетке)
          А так, мне в целом стандарт странным кажется. Несмотря на наличие firebase и мозилловскиго сервера нужен все равно свой для рассылки. Куда логичней были бы каналы подписки и апи для отравки сообщения сразу всем подписанным клиентам, поскольку задача «отправить одно сообщение сразу всем» возникает гораздо чаще, нежели «отправить кому-то конкретному».


          1. Evengard
            13.03.2017 14:42

            В итоге мне кажется накостылять что-то своё будет проще и эффективней как ни странно… Больше ресурсозатрат конечно будет на стороне своего сервера, но как бонус снимается зависимость от непонятных внешних сервисов, на которых никакого влияния нет и которые предоставляются as-is.


        1. den007
          14.03.2017 06:16

          Все тут правильно, Вы не задумывались как будете слать уведомления со своего сервера браузеру? Ответ весьма прост никак. Первой преградой будет динамический ip, а второй и непреодолимой это НАТ. Ну да ладно живем-то во времена НАТа, пускай браузер сам подключаться к 100500 сайтам, на которые подписан пользователь. В результате что мы имеем? 100500 бессмысленных постоянных соединений с сайтами на клиенте и то же самое на другом конце провода. Как результат бессмысленная трата ресурсов. В данном случае единый/е сервера к которым подключаются браузер имея 1 соединение и сайты которые ему шлют свои уведомления. Речь идет, разумеется, об уведомления при закрытом сайте/браузере, так как при открытой вкладке с браузером можно использовать WebSocket, что в данном случае оправдано (когда человек на сайте).
           
          За последнее время мы так привыкли ко всем этим API, что забываем как это работает на низком уровне, а ведь ничего особо не изменилось.
           
          P.S. Я надеюсь я правильно Вас понял.


  1. timon_aeg
    13.03.2017 19:02

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


    1. ghost404
      13.03.2017 19:03

      Ну как сказать не автоматизируется. Уведомления это как email сообщения. Они работают через сторонние сервисы. Вопрос осмысленности такого тестирования я не поднимаю.


      Некоторые тестируют отправляемые и доставляемые email сообщения поднимая тестовый SMTP сервер. Так и тут. Никто не мешает, при желании, поднять тестовый Service Worker и тестировать уведомления через него.


      Тестировать работу самого Service Worker-а уже сложнее так как он работает вне контекста текущей страницы.


      1. timon_aeg
        13.03.2017 19:47

        Спасибо, попробую в этом направлении копать.
        Пока что я видел двухлетней давности инструкцию по отладке уведомлений в Хроме от developers.google, которая не работает. И велосипеды с прокси сервером, через который идет трафик браузера, но тут вопрос чтения содержимого из-за обязательного https.


      1. Apathetic
        13.03.2017 22:50

        Тестировать работу сервис-воркера не сложно. Можно через Selenium в браузере — события элементарно эмулируются через self.dispatchEvent. Юнит-тестирование проводится на моках.


  1. Wouw
    14.03.2017 01:04

    Подскажите, есть ли c++ библиотеки для реализации отправки сообщений?


    1. iSage
      14.03.2017 02:27

      Отправка сообщений делается обычным хттп-запросом, никакой магии.


  1. tixit
    14.03.2017 12:10

    Вводить в заблуждение читателей о бесплатности использования Firebase довольно подлое занятие. И я это вижу практически в каждой статье про всякие Амазоны, Гуглы и прочие платформы. А тем не менее бесплатными являются только некий мизерный объем услуг. У Firebase к примеру это 100 одновременных подключений к их сервису. Т.е. бесплатным будем только период тестирования и первые дни запуска проекта. Как только у проекта появляется хоть какая-то небольшая популярность, эти бесплатности начинает активно кушать деньги. Вот сейчас сижу изучаю проект обильно напичканный этими бесплатностями, которые съедает 70% прибыли. В год он приносит 5 млн рублей, но на всякие CDN, PUSH, ELASTIC и прочие плюшки тратится 3.5 млн. рублей. При этом анализ нагрузки показывает, что все эти условно-бесплатные услуги можно будет перевести на собственные выделенные серверы, и сумма расходов на бесплатные услуги снизится в 10 раз.
    Так что статью надо было назвать «Web PUSH Notifications быстро и просто, но очень дорого!», а потом показать что существуют действительно полностью бесплатные решения.


    1. ghost404
      14.03.2017 12:17

      push-уведомления через Firebase бесплатные. Не знаю с чего вы взяли что за них нужно платить. Если использовать сторонние сервисы для отправки уведомлений такие как OneSignal и PushAll, то естественно это будет стоит денег, о чем я писал в одном из комментариев.


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


      1. tixit
        14.03.2017 13:46

        Почему вы так упорно называете бесплатным то, за что придется платить? И как минимум $25. Как я уже сказал бесплатен только тарифный план SPARK, который поддерживает только 100 одновременных клиентских соединений с сервером. Это очень мало. Как говорят в google «Щедрые ограничения для любителей». Их хватает только на период разработки, тестирования и несколько дней после запуска проекта. Как только начинается коммерческая эксплуатация проекта, включается тарифный план FLAME $25 per month просто в силу того, что с проектом работает больше 100 пользователей одновременно. А через месяц-другой, ваш проект уже на тарифном плане BLAZE просто потому что у вас превысились лимиты по трафику или по объему занятого места. Поэтому уместно писать бесплатный сервис для разработчиков. но никак не для владельцев проекта.


        1. ghost404
          14.03.2017 14:48

          На официальном сайте Firebase, в разделе цены, вполне четко описано какие услуги платные, а какие нет


          Included Free
          Analytics, App Indexing, Authentication, Cloud Messaging, Crash Reporting, Dynamic Links, Invites, Notifications & Remote Config

          push-уведомления относятся к категории бесплатных услуг.
          Еще раз. С чего вы решили что уведомления платные?


        1. ghost404
          14.03.2017 14:54

          Вы похоже перепутали Firebase Realtime Database и Firebase Cloud Messaging.


      1. BupycNet
        14.03.2017 21:57

        Насчет оплаты OneSignal и PushAll. OneSignal по сути по поводу отправки бесплатен, но скорость на нем ограничена, опытным путем коллег из Медузы было выяснено, что скорость там примерно 500 уведомлений в секунду.
        Насчет наших цен, мы у себя описали некие ограничения скорости в 100 в секунду на бесплатном тарифе, 500 на платном и выше если будет нужно. На деле же эти ограничения практически не действуют, даже на бесплатном тарифе сейчас можно более 3000 уведомлений FCM отправить за 1,5 секунды при условии что это массовая рассылка. Более того для интересных проектов мы предоставляем услуги бесплатно и делаем фичи если им это необходимо, надо лишь мне написать.

        Также отвечу на предположение выше о 100 соединениях. Хорошо, 100 соединений, в среднем при открытом TLS соединении с гуглом 1000 уведомлений отправляется за 1 секунду. 100 соединений дает нам 100 000 уведомлений в секунду. Куда вам столько?
        Да если вы шлете индивидуальные уведомления то гугл их обрабатывает значительно быстрее, примерно на уровне пинга до гугла.
        У нас по логам 1 сообщение при существующем хендшейке ушло за 0.04 секунды. На один поток выходит 50 уведомлений в секунду. На 100 потоков это 5000 уведомлений в секунду. Это 18 миллионов уведомлений в час. Даже если каждый пользователь принимает 10 уведомлений в час это 1.8 миллионов активных пользователей.

        Но коннекты на FCM не ограничены, также у гугла есть отправка через XMPP, которая должна быть в разы быстрее.


  1. FluorescentHallucinogen
    14.03.2017 12:26
    +3

    Отвечу сразу на несколько комментариев разных людей одним сообщением.


    Нельзя отправить сообщение с клиента. То есть отправить запрос с помощью AJAX или веб-формы на сервер, чтобы тот отправил push-уведомление нам на клиентскую сторону. Не работает.

    Можно. Работает: https://gauntface.github.io/simple-push-demo/.


    Не смотря на заявленную поддержку Firefox, в нем уведомления не работают.

    Работают. Проверял на Windows и Android.


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

    Смотря что подразумевается под сервером.


    См.: https://github.com/mozilla-services/autopush.


    Есть отличный учебник по Web Push — https://web-push-book.gauntface.com.


    К этой книге прилагается демонстрационный проект на Node.js: https://github.com/gauntface/web-push-book/tree/master/src/demos/node-server.


    Использовать Firebase Cloud Messaging не обязательно, есть Web Push Libraries. Для Node.js: https://github.com/web-push-libs/web-push, для PHP: https://github.com/web-push-libs/web-push-php.


    Библиотека Firebase скрывает в себе много тайн и её исследование могло бы дать ответы на некоторые вопросы, но это уже выходит за рамки этой статьи.

    А жаль, там много интересного. ) Думаю, я созрел для своей первой статьи для Хабра. :)


    1. ghost404
      14.03.2017 12:26

      Можно. Работает: https://gauntface.github.io/simple-push-demo/.
      Работают. Проверял на Windows и Android.

      Как мне уже объяснил iSage и я сам об этом подозревал — это Firebase не может отправлять уведомления, а если реализовывать push-уведомления нативными средствами, то все работает. Что у вас и сделано.


      А жаль, там много интересного. ) Думаю, я созрел для своей первой статьи для Хабра. :)

      Я вижу что описанного материала многим недостаточно. И хотел бы написать статью/продолжение в которой описать больше технических тонкостей и альтернативных реализаций.
      Готов уступить вам эту тему.


      1. FluorescentHallucinogen
        14.03.2017 21:02

        Попробуйте Firebase Cloud Messaging JavaScript Quickstart и отпишитесь здесь о результате.


        Проверял отправку со стороны клиента в браузере с помощью Fetch на Chrome и Firefox, на Windows и Android — всё работает.


        1. ghost404
          15.03.2017 13:13

          Форкнул репозиторий и открыл на GitHub Pages. Протестировал через curl в Chrome и Firefox на Windows. Результат, мягко говоря, ошеломляет.


          В Chrome все доходит как и должно и уведомление отображается. В Firefox уведомление тоже доходит, но не отображается. Отрабатывает метод messaging.onMessage() на странице и печатает тело уведомления в блоке #messages. Через fetch я получил тот же результат. В Chrome этот метод не отрабатывает.


          Получается в Firefox нужно использовать метод messaging.onMessage() и в нем реализовывать показ уведомления вручную. Поскольку этот метод реализован на странице, а не в Service Worker, то мы сталкиваемся с рядом проблем:


          • Нельзя получить уведомление если сайт не открыт в данный момент;
          • Нельзя получить уведомление при закрытом браузере;
          • Если сайт открыт в нескольких вкладках, то скрипт отрабатывает во всех вкладках.

          Это лишь результаты беглого осмотра. Возможно я что-то упустил. У вас другие результаты?


          И спасибо за ссылку. Добавил в статью.


          PS: Странно то что у меня и без messaging.onMessage() в Firefox уведомления иногда отображались.


          1. FluorescentHallucinogen
            15.03.2017 15:30
            +2

            В Chrome этот метод не отрабатывает.

            Какой именно метод?


            К этому проекту прилагается видео https://youtu.be/BsCBCudx58g, в котором подробно объясняется, что к чему.


            Обратите особое внимание на примечание о фокусе на странице https://github.com/firebase/quickstart-js/tree/master/messaging:


            When the app has the browser focus, the received message is handled through the onMessage callback in index.html. When the app does not have browser focus then the setBackgroundMessageHandler callback in firebase-messaging-sw.js is where the received message is handled.

            The browser gives your app focus when both:
            1. Your app is running in the currently selected browser tab.
            2. The browser tab's window currently has focus, as defined by the operating system.

            Следует также принять во внимание, что поведение одного и того же браузера на разных платформах может различаться. Например, Firefox для Android и Mac могут после закрытия работать в фоновом режиме и, соответственно, принимать push-уведомления, а Firefox для Windows — нет.


            Можно передавать сообщения из service worker'а на страницу: https://web-push-book.gauntface.com/chapter-05/04-common-notification-patterns/#message-page-from-a-push-event


            Советую всё-таки прочитать книгу https://web-push-book.gauntface.com полностью, в ней есть ответы почти на все вопросы о web push. ;)


            1. ghost404
              15.03.2017 17:56

              Точно. Вся проблема была в messaging.onMessage(). Теперь я понял почему не работало без него. Добавил обработчик и сразу все, везде заработало.


              Спасибо большое.


    1. FluorescentHallucinogen
      15.03.2017 16:06

      Кстати, есть ещё отличная книга о service workers — https://serviceworke.rs, в ней есть раздел о web push — https://serviceworke.rs/web-push.html с примерами использования https://github.com/web-push-libs/web-push.


      1. ghost404
        15.03.2017 16:44

        Спасибо. Добавил ссылки в статью