
Добро пожаловать в школу CODEдейства и волшебства!
На сегодняшнем занятии мы с вами узнаем, как использовать малоизвестный тандем Web Push + Service Workers (SW). Я приоткрою вам завесу: расскажу о способе удерживать аудиторию маглов благодаря технологии Web Push и о том, чем это может быть полезно для редакций сайтов и прочих интернет-сервисов.
Текст продолжает статью о базовом применении SW. Из комментариев и вопросов я понял, что людям интересно направление SW + Web Push. Давайте вместе разберёмся с тем, что это такое, и как использовать эту магическую пару на практике.
Что такое Push Notification?
Вы принимаете оповещения на свою электронную почту: заходите в почтовый клиент и смотрите входящие письма. В данном случае это технология pull, то есть вы заходите на сайт и «тянете» с него данные, когда они вам нужны.
В случае же с push-уведомлениями ресурс проталкивает новые данные вам сам. При этом вы сразу получаете самые свежие данные, ведь в этой технологии нет определённого периода проверки данных, они приходят в режиме онлайн. Использование пушей не ограничивается получением уведомлений. Так, через push-технологию можно синхронизировать данные при обновлении.
Push-уведомления — это небольшие всплывающие окна. Они могут появляться на экране, где есть область оповещений или есть возможность вывода на экран принятых данных. Push-уведомления работают даже тогда, когда пользователь ушёл с сайта. Это означает, что вы можете доставлять сообщения о новых материалах и новых событиях, чтобы привлечь внимание пользователя в ваше приложение.

Не забывайте о поддержке в браузерах. Так в IE, Edge и Safari ваши Push-уведомления работать не будут.

Заготовки для магической связки SW и Web-push
Для написания своего собственного SW для работы с Web-push нам как всегда понадобятся:
Всё, что нужно сделать, это в index.html подключить index.js, в котором будет происходить регистрация файла sw.js.
В файле server.js я укажу лишь эндпоинт (точка входа в API серверного приложения) для регистрации push-уведомлений.
Пример кода файла server.js
// Мы используем библиотеку web-push, чтобы скрыть детали реализации связи
// между сервером приложений и службой push.
// Для получения дополнительной информации
// см. https://tools.ietf.org/html/draft-ietf-webpush-protocol
// и https://tools.ietf.org/html/draft-ietf-webpush-encryption.
var webPush = require('web-push');
// Про GCM_API_KEY вы можете подробнее узнать из
// https://developers.google.com/cloud-messaging/
webPush.setGCMAPIKey(process.env.GCM_API_KEY || null);
// В данном примере мы будем рассматривать только route'ы в express.js
module.exports = function(app, route) {
app.post(route + 'register', function(req, res) {
res.sendStatus(201);
});
app.post(route + 'sendNotification', function(req, res) {
setTimeout(function() {
// Для отправки сообщения с payload, подписка должна иметь ключи 'auth' и 'p256dh'.
webPush.sendNotification({
endpoint: req.body.endpoint,
TTL: req.body.ttl,
keys: {
p256dh: req.body.key,
auth: req.body.authSecret
}
}, req.body.payload)
.then(function() {
res.sendStatus(201);
})
.catch(function(error) {
res.sendStatus(500);
console.log(error);
});
}, req.query.delay * 1000);
});
};
В статье мы рассмотрим варианты отправки push-уведомлений и способы их применения. Давайте знакомиться с магией вне Хогвартса вместе.
Push Payload
Этот простейшее магическое заклинание показывает, как отправлять и получать строки, но данные могут быть извлечены из push-сообщения в различных форматах: строки, буфер ArrayBuffer, BLOB-объект в JSON.
Способ применения
Если вам просто нужны push-уведомления — этот пример для вас. Сообщение может доставлять не только текст, но и payload — обогащенные данные для приложения. Код ниже демонстрирует, как вы можете доставлять payload для вашего приложения.
Для демонстрации мы используем данные из текстового поля, которые будут отправлены на сервер и затем отображены в виде push-уведомления через SW.
index.js
var endpoint;
var key;
var authSecret;
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
// Используем PushManager, чтобы получить подписку пользователя из пуш-сервиса.
return registration.pushManager.getSubscription()
.then(function(subscription) {
// Если подписка уже существует возвращаем ее.
if (subscription) {
return subscription;
}
// В противном случае, подписываем пользователя.
// userVisibleOnly - это флаг указывающий, что возвращенная push-подписка
// будет использоваться только для сообщений,
// эффект которых будет виден для пользователя.
return registration.pushManager.subscribe({ userVisibleOnly: true });
});
}).then(function(subscription) {
// Получаем public key для пользователя.
var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
key = rawKey
? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey)))
: '';
var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
authSecret = rawAuthSecret
? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret)))
: '';
endpoint = subscription.endpoint;
// Отправляем детали о подписке на сервер используя Fetch API
fetch('./register', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
endpoint: subscription.endpoint,
key,
authSecret,
}),
});
});
// Для демонстрации функционала.
// Данный код на "Боевых" приложениях не нужен, т.к. генерация уведомлений всегда происходит на сервере.
document.getElementById('doIt').onclick = function() {
var payload = document.getElementById('notification-payload').value;
var delay = document.getElementById('notification-delay').value;
var ttl = document.getElementById('notification-ttl').value;
fetch('./sendNotification', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
endpoint: endpoint,
payload: payload,
delay: delay,
ttl: ttl,
key: key,
authSecret: authSecret
}),
});
};
service-worker.js
// Регистрируем функцию на событие 'push'
self.addEventListener('push', function(event) {
var payload = event.data ? event.data.text() : 'Alohomora';
event.waitUntil(
// Показываем уведомление с заголовком и телом сообщения.
self.registration.showNotification('My first spell', {
body: payload,
})
);
});
Rich Notifications
Усложним предыдущий вариант и добавим спецэффектов, здесь всё зависит от ваших желаний и фантазии. Нам поможет полное Notification API. API предоставляет интерфейс для использования «живых» push-уведомлений пользователю c указанием локали, шаблоном вибрации, изображения.
Способ применения
Пример схож с тем, что описан выше, но позволяет использовать более расширенное Notification API, чтобы выбирать изображение, выставлять локаль и шаблон уведомления — то есть делать уведомление уникальным.
service-worker.js
// Ключевое отличие по сравнению с Push Payload именно в использовании
// Notitfication API в SW
self.addEventListener('push', function(event) {
var payload = event.data
// У нас всё-таки волшебный мир с фантастическими тварями, поэтому
// try.. catch мы не ставим ¯\_(ツ)_/¯
? JSON.parse(event.data)
: {
name: 'Expecto patronum!',
icon: 'buck.jpg',
locale: 'en'
};
event.waitUntil(
// Показываем уведомление с заголовком и телом сообщения.
// Устанавливаем иные параметры:
// * язык
// * шаблон вибрации
// * изображение
// имеется очень много параметров, о которых вы можете узнать тут
// https://notifications.spec.whatwg.org/
self.registration.showNotification('Summoning spell', {
lang: payload.locale,
body: payload.name,
icon: payload.icon,
vibrate: [500, 100, 500],
})
);
});
Push Tag
Следующее заклинание будет демонстрировать аккумулирование и замену предыдущего заклинания более сильным. Используем тег для уведомления, чтобы заменить старые уведомления новыми. Позволяет показывать маглам только актуальную информацию или сворачивать несколько уведомлений в одно.
Способ применения
Вариант подойдёт для тех приложений, где имеются чаты или уведомления о новом контенте (примеры: Tproger и Tinder). Ниже код демонстрирует, как управлять очередью уведомлений, чтобы предыдущие уведомления можно было отбросить или объединить в одно уведомление. Это полезно, чтобы иметь fallback на случай, если мы написали чат, где можно редактировать сообщения. Клиент увидит не тонну уведомлений с исправлениями, а всего лишь одно.
service-worker.js
var num = 1;
self.addEventListener('push', function(event) {
event.waitUntil(
// Показываем уведомление с заголовком и телом сообщения.
// Число, которое увеличивается для каждого полученного уведомления.
// Поле тега позволяет заменить старое уведомление на новое
// (уведомление с тем же тегом другого заменит его)
self.registration.showNotification('Attacking Spell', {
body: ++num > 1 ? 'Bombarda Maxima' : 'Bombarda',
tag: 'spell',
})
);
});
Push Clients
Пришло время для «непростительных заклинаний». Напомню, почему они непростительные:
любое чрезмерное применение этих заклинаний к маглам карается пожизненным заключением в Азкабан. Поэтому главное — не надоедать!
Когда магл нажмет на уведомление, сгенерированное из push-события, оно сфокусирует его на вкладке приложения или даже повторно откроет его, если оно было закрыто.
Способ применения
Ниже код для трёх случаев использования доставки уведомлений в зависимости от состояния приложения.
Он может распознавать, когда вы находитесь на странице или нужно переключиться на открытую вкладку или повторно открыть вкладку.
Способ поможет вам уве��ичить конверсию приложения, если вы хотите вернуть магла. Только не перегибайте палку, маглам может не понравиться излишнее внимание приложения.
Самые классические примеры использования:
- пришло сообщение в чат (Tinder),
- интересная новость (Tproger),
- обновилась задача в баг-трекере,
- успешно/неуспешно прошел CI перед релизом,
- клиент оплатил заказ/заключил сделку (будь то интернет-магазин или CRM).
Во всех этих случаях при клике в push клиенту откроется наше приложение или он будет сфокусирован уже на открытой вкладке.
service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener('push', function(event) {
event.waitUntil(
// Получить список клиентов для SW
self.clients.matchAll().then(function(clientList) {
// Проверяем, есть ли хотя бы один сфокусированный клиент.
var focused = clientList.some(function(client) {
return client.focused;
});
var notificationMessage;
if (focused) {
notificationMessage = 'Imperio! You\'re still here, thanks!';
} else if (clientList.length > 0) {
notificationMessage = 'Imperio! You haven\'t closed the page, ' +
'click here to focus it!';
} else {
notificationMessage = 'Imperio! You have closed the page, ' +
'click here to re-open it!';
}
// Показывать уведомление с заголовком «Unforgiveable Curses»
// и телом в зависимости от состоянию клиентов SW
// (три разных тела:
// * 1, страница сфокусирована;
// * 2, страница по-прежнему открыта, но не сфокусирована;
// * 3, страница закрыта).
return self.registration.showNotification('Unforgiveable Curses', {
body: notificationMessage,
});
})
);
});
// Регистрируем обработчик события 'notificationclick'.
self.addEventListener('notificationclick', function(event) {
event.waitUntil(
// Получаем список клиентов SW.
self.clients.matchAll().then(function(clientList) {
// Если есть хотя бы один клиент, фокусируем его.
if (clientList.length > 0) {
return clientList[0].focus();
}
// В противном случае открываем новую страницу.
return self.clients.openWindow('our/url/page');
})
);
});
Push Subscription
Пришло время завладеть разумом наших маглов. Маглы называют это «телепатией» или чтением мыслей, но будем делать иначе. Давайте научимся помещать нашу информацию и заставлять привязываться к нашему приложению. Этот пример показывает, как использовать push-уведомления с управлением подпиской, позволяя пользователям подписаться на приложение и поддерживать связь с ним. Стараемся помнить об Азкабане!
Способ применения
После того, как SW зарегистрирован, клиент проверяет, подписан ли он на сервис уведомлений. В зависимости от этого устанавливается текст кнопки.
После успешной подписки (index.js::pushManager.subscribe) клиент отправляет post-запрос на сервер приложений для регистрации подписки.
Сервер периодически отправляет уведомление с помощью библиотеки web-push на все зарегистрированные эндпоинты. Если эндпоинт больше не зарегистрирован (подписка истекла или отменена), текущая подписка удаляется из списка подписок.
После успешной отписки (index.js::pushSubscription.unsubscribe) клиент отправляет post-запрос на сервер приложений, чтобы отменить регистрацию подписки. Сервер больше не отправляет уведомления. SW также следит за событиями pushsubscriptionchange и resubscribes.
Подписка может быть отменена пользователем вне страницы в настройках браузера или UI-уведомлений. В этом случае бекенд перестанет отправлять уведомления, но фронтенд об этом не узнает. Чтобы магия работала, важно периодически проверять, активна ли регистрация в службе уведомлений.
index.js
// Для простоты будем использовать кнопку.
// На боевой версии лучше использовать события.
var subscriptionButton = document.getElementById('subscriptionButton');
// Поскольку объект подписки требуется в нескольких местах, давайте создадим метод,
// который возвращает Promise.
function getSubscription() {
return navigator.serviceWorker.ready
.then(function(registration) {
return registration.pushManager.getSubscription();
});
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(function() {
console.log('SW registered');
subscriptionButton.removeAttribute('disabled');
});
getSubscription()
.then(function(subscription) {
if (subscription) {
console.log('Already invaded', subscription.endpoint);
setUnsubscribeButton();
} else {
setSubscribeButton();
}
});
}
// Получить «registration» от SW и создать новую
// подписку с помощью `registration.pushManager.subscribe`.
// Затем зарегистрировать новую подписку, отправив POST-запрос.
function subscribe() {
navigator.serviceWorker.ready.then(function(registration) {
return registration.pushManager.subscribe({ userVisibleOnly: true });
}).then(function(subscription) {
console.log('Legilimens!', subscription.endpoint);
return fetch('register', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
endpoint: subscription.endpoint
})
});
}).then(setUnsubscribeButton);
}
// Получить существующую подписку от SW,
// отменить подписку (`subscription.unsubscribe ()`) и
// отменить регистрацию на сервере с помощью POST-запроса
// для прекращения отправки push-сообщений.
function unsubscribe() {
getSubscription().then(function(subscription) {
return subscription.unsubscribe()
.then(function() {
console.log('Unsubscribed', subscription.endpoint);
return fetch('unregister', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
endpoint: subscription.endpoint
})
});
});
}).then(setSubscribeButton);
}
// Для демонстрации (или тренировок). Изменяем текст кнопки.
function setSubscribeButton() {
subscriptionButton.onclick = subscribe;
subscriptionButton.textContent = 'Open mind!';
}
function setUnsubscribeButton() {
subscriptionButton.onclick = unsubscribe;
subscriptionButton.textContent = 'Protego!';
}
service-worker.js
// Слушаем событие 'push'.
self.addEventListener('push', function(event) {
event.waitUntil(self.registration.showNotification('Your mind', {
body: 'Wizard invaded to your mind!'
}));
});
// Слушаем событие 'pushsubscriptionchange', которое запускается,
// когда истекает срок подписки.
// Подписываемся снова и регистрируем новую подписку на сервере,
// отправив POST-запрос.
// На боевом приложении скорее всего будет использоваться ID или token
// для идентификации пользователя.
self.addEventListener('pushsubscriptionchange', function(event) {
console.log('Spell expired');
event.waitUntil(
self.registration.pushManager.subscribe({ userVisibleOnly: true })
.then(function(subscription) {
console.log('Another invade! Legilimens!', subscription.endpoint);
return fetch('register', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
endpoint: subscription.endpoint
})
});
})
);
});
server.js
var webPush = require('web-push');
var subscriptions = [];
var pushInterval = 10;
webPush.setGCMAPIKey(process.env.GCM_API_KEY || null);
// Отправляем уведомление push-сервису.
// Удаляем подписку из общего массива `subscriptions`,
// если push-сервис отвечает на ошибку или подписка отменена или истекла.
function sendNotification(endpoint) {
webPush.sendNotification({
endpoint: endpoint
}).then(function() {
}).catch(function() {
subscriptions.splice(subscriptions.indexOf(endpoint), 1);
});
}
// В реальных условиях приложение отправляет уведовление только в случае
// возникновения события.
// Чтобы имитировать его, сервер отправляет уведомление каждые `pushInterval` секунд
// каждому подписчику
setInterval(function() {
subscriptions.forEach(sendNotification);
}, pushInterval * 1000);
function isSubscribed(endpoint) {
return (subscriptions.indexOf(endpoint) >= 0);
}
module.exports = function(app, route) {
app.post(route + 'register', function(req, res) {
var endpoint = req.body.endpoint;
if (!isSubscribed(endpoint)) {
console.log('We invaded into mind ' + endpoint);
subscriptions.push(endpoint);
}
res.type('js').send('{"success":true}');
});
// Unregister a subscription by removing it from the `subscriptions` array
app.post(route + 'unregister', function(req, res) {
var endpoint = req.body.endpoint;
if (isSubscribed(endpoint)) {
console.log('It was counterspell from ' + endpoint);
subscriptions.splice(subscriptions.indexOf(endpoint), 1);
}
res.type('js').send('{"success":true}');
});
};
Ещё разок о заклинаниях
Выше мы рассмотрели магические способы использования SW и Web Push для приложений.
Данный тандем таит в себе множество интересных применений.
Если вам нужно лишь иногда зазывать магла к себе в приложение или сообщать ему о об исправлениях или изменении статуса его заказа, то используйте Push Payload. Можно добавить немного фантазии и заиспользовать Notification API — тогда цвета и иконка вашего приложения будет видны пользователю в Rich Push.
Если же вы желаете завладеть всем вниманием магла и установить с ним контакт — примеры Push Client и Push Subscription для вас. Главное — помните об Азкабане, иначе вы начнёте терять свою аудиторию.
Жду ваших комментариев и пожеланий на следующую тему. От себя добавлю, что хотелось бы поговорить и обсудить тему работы SW + React/Redux-приложений и способы ускорения. Будет полезно?