Мы собираемся рассмотреть некоторые распространенные шаблоны реализации веб-push.
Это потребует использования нескольких различных API, доступных в сервисном работнике.
Событие закрытия уведомления
В последнем разделе мы увидели, как мы можем прослушивать события notificationclick
.
Существует также событие notificationclose
, которое вызывается, если пользователь отклоняет одно из ваших уведомлений (т. е. вместо того, чтобы щелкнуть уведомление, пользователь щелкает крестик или смахивает уведомление).
Это событие обычно используется для аналитики для отслеживания взаимодействия пользователей с уведомлениями.
self.addEventListener('notificationclose', function (event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
Добавление данных в уведомление
При получении push-сообщения обычно имеются данные, которые полезны только в том случае, если пользователь нажал на уведомление. Например, URL-адрес, который должен открываться при нажатии на уведомление.
Самый простой способ получить данные из push-события и прикрепить их к уведомлению — добавить параметр data
к объекту параметров, передаваемому в showNotification()
, например:
const options = {
body:
'This notification has data attached to it that is printed ' +
"to the console when it's clicked.",
tag: 'data-notification',
data: {
time: new Date(Date.now()).toString(),
message: 'Hello, World!',
},
};
registration.showNotification('Notification with Data', options);
Доступ к данным внутри обработчика кликов можно получить с помощью event.notification.data
.
const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
console.log(` ${key}: ${notificationData[key]}`);
});
console.log('');
Открыть окно
Одним из наиболее распространенных ответов на уведомление является открытие окна/вкладки по определенному URL-адресу. Мы можем сделать это с помощью API clients.openWindow()
.
В нашем событии notificationclick
мы запустили такой код:
const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);
В следующем разделе мы рассмотрим, как проверить, открыта ли уже страница, на которую мы хотим направить пользователя, или нет. Таким образом, мы можем сосредоточиться на открытой вкладке, а не открывать новые вкладки.
Фокус на существующем окне
Когда это возможно, нам следует фокусировать окно, а не открывать новое окно каждый раз, когда пользователь щелкает уведомление.
Прежде чем мы рассмотрим, как этого добиться, стоит подчеркнуть, что это возможно только для страниц вашего источника . Это связано с тем, что мы можем видеть только те открытые страницы, которые принадлежат нашему сайту. Это не позволяет разработчикам видеть все сайты, которые просматривают их пользователи.
Взяв предыдущий пример, мы изменим код, чтобы проверить, открыт ли уже /demos/notification-examples/example-page.html
.
const urlToOpen = new URL(examplePage, self.location.origin).href;
const promiseChain = clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
Давайте пройдемся по коду.
Сначала мы анализируем нашу примерную страницу с помощью URL API. Это изящный трюк, который я позаимствовал у Джеффа Посника . Вызов new URL()
с объектом location
вернет абсолютный URL-адрес, если переданная строка является относительной (т. е. /
станет https://example.com/
).
Мы делаем URL-адрес абсолютным, чтобы позже можно было сопоставить его с URL-адресом окна.
const urlToOpen = new URL(examplePage, self.location.origin).href;
Затем мы получаем список объектов WindowClient
, который представляет собой список открытых в данный момент вкладок и окон. (Помните, что это вкладки только для вашего происхождения.)
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
Параметры, переданные в matchAll
сообщают браузеру, что мы хотим искать только клиентов типа «окно» (т. е. просто искать вкладки и окна и исключать веб-работников). includeUncontrolled
позволяет нам искать все вкладки из вашего источника, которые не контролируются текущим сервис-воркером, то есть сервис-воркером, выполняющим этот код. Как правило, при вызове matchAll()
вам всегда нужно, чтобы includeUncontrolled
имел значение true.
Мы фиксируем возвращенное обещание как promiseChain
, чтобы позже передать его в event.waitUntil()
, сохраняя работоспособность нашего сервис-воркера.
Когда обещание matchAll()
разрешается, мы перебираем возвращенные клиенты окна и сравниваем их URL-адреса с URL-адресом, который мы хотим открыть. Если мы найдем совпадение, мы сосредоточим внимание на этом клиенте, что привлечет внимание пользователей к этому окну. Фокусировка осуществляется с помощью вызова matchingClient.focus()
.
Если мы не можем найти подходящего клиента, мы открываем новое окно, как и в предыдущем разделе.
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
Объединение уведомлений
Мы видели, что добавление тега к уведомлению приводит к замене любого существующего уведомления с тем же тегом.
Однако вы можете усложнить свертывание уведомлений с помощью Notifications API. Рассмотрим приложение чата, где разработчик может захотеть, чтобы в новом уведомлении отображалось сообщение, похожее на «У вас есть два сообщения от Мэтта», а не просто отображалось последнее сообщение.
Вы можете сделать это или манипулировать текущими уведомлениями другими способами, используя API Registration.getNotifications() , который дает вам доступ ко всем видимым в данный момент уведомлениям для вашего веб-приложения.
Давайте посмотрим, как мы могли бы использовать этот API для реализации примера чата.
Предположим, что в нашем чат-приложении каждое уведомление содержит некоторые данные, включая имя пользователя.
Первое, что нам нужно сделать, это найти все открытые уведомления для пользователя с определенным именем пользователя. Мы получим registration.getNotifications()
, пройдем по ним и проверим notification.data
для определенного имени пользователя:
const promiseChain = registration.getNotifications().then((notifications) => {
let currentNotification;
for (let i = 0; i < notifications.length; i++) {
if (notifications[i].data && notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
});
Следующим шагом будет замена этого уведомления новым уведомлением.
В этом приложении для поддельных сообщений мы будем отслеживать количество новых сообщений, добавляя счетчик к данным нашего нового уведомления и увеличивая его с каждым новым уведомлением.
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// We have an open notification, let's do something with it.
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
// Remember to close the old notification.
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});
Если в данный момент отображается уведомление, мы увеличиваем количество сообщений и соответствующим образом устанавливаем заголовок и текст уведомления. Если уведомлений нет, мы создаем новое уведомление с newMessageCount
, равным 1.
В результате первое сообщение будет выглядеть так:
Второе уведомление свернёт уведомления следующим образом:
Преимущество этого подхода в том, что если ваш пользователь увидит, что уведомления появляются одно поверх другого, это будет выглядеть более связно, чем просто замена уведомления последним сообщением.
Исключение из правил
Я уже говорил, что вы должны показывать уведомление при получении push-уведомления, и это верно в большинстве случаев. Единственный сценарий, в котором вам не нужно показывать уведомление, — это когда у пользователя открыт и сосредоточен ваш сайт.
Внутри вашего push-события вы можете проверить, нужно ли вам показывать уведомление или нет, исследуя оконные клиенты и ища конкретное окно.
Код для получения всех окон и поиска сфокусированного окна выглядит следующим образом:
function isClientFocused() {
return clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let clientIsFocused = false;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
clientIsFocused = true;
break;
}
}
return clientIsFocused;
});
}
Мы используем clients.matchAll()
для получения всех наших оконных клиентов, а затем перебираем их в цикле, проверяя focused
параметр.
Внутри нашего push-события мы бы использовали эту функцию, чтобы решить, нужно ли нам показывать уведомление:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
console.log("Don't need to show a notification.");
return;
}
// Client isn't focused, we need to show a notification.
return self.registration.showNotification('Had to show a notification.');
});
event.waitUntil(promiseChain);
Отправка сообщения на страницу из push-события
Мы видели, что вы можете пропустить показ уведомления, если пользователь в данный момент находится на вашем сайте. Но что, если вы все равно хотите сообщить пользователю о том, что событие произошло, но уведомление слишком жесткое?
Один из подходов — отправить сообщение от работника службы на страницу. Таким образом, веб-страница может отображать пользователю уведомление или обновление, информируя его о событии. Это полезно в ситуациях, когда тонкое уведомление на странице лучше и удобнее для пользователя.
Допустим, мы получили push-уведомление, проверили, что наше веб-приложение в данный момент сфокусировано, а затем можем «отправить сообщение» на каждую открытую страницу, вот так:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
windowClients.forEach((windowClient) => {
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString(),
});
});
} else {
return self.registration.showNotification('No focused windows', {
body: 'Had to show a notification instead of messaging each page.',
});
}
});
event.waitUntil(promiseChain);
На каждой странице мы прослушиваем сообщения, добавляя прослушиватель событий сообщений:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log('Received a message from service worker: ', event.data);
});
В этом прослушивателе сообщений вы можете делать все, что захотите: отображать собственный пользовательский интерфейс на своей странице или полностью игнорировать сообщение.
Также стоит отметить, что если вы не определите прослушиватель сообщений на своей веб-странице, сообщения от сервис-воркера ничего не сделают.
Кэшировать страницу и открыть окно
Один из сценариев, который выходит за рамки данного руководства, но который стоит обсудить, заключается в том, что вы можете улучшить общий UX вашего веб-приложения, кэшируя веб-страницы, которые, как вы ожидаете, пользователи будут посещать после нажатия на ваше уведомление.
Для этого необходимо, чтобы ваш сервис-воркер был настроен на обработку событий fetch
, но если вы реализуете прослушиватель событий fetch
, убедитесь, что вы используете его в своем событии push
, кэшируя страницу и ресурсы, которые вам понадобятся, прежде чем показывать уведомление.
Совместимость с браузером
Событие notificationclose
Clients.openWindow()
ServiceWorkerRegistration.getNotifications()
clients.matchAll()
Для получения дополнительной информации ознакомьтесь с введением в пост Service Workers .
Куда идти дальше
- Обзор веб-push-уведомлений
- Как работает Push
- Подписка пользователя
- Разрешение UX
- Отправка сообщений с помощью библиотек Web Push
- Веб-пуш-протокол
- Обработка push-событий
- Отображение уведомления
- Поведение уведомлений
- Общие шаблоны уведомлений
- Часто задаваемые вопросы по push-уведомлениям
- Распространенные проблемы и сообщения об ошибках
Лаборатории кода
,Мы собираемся рассмотреть некоторые распространенные шаблоны реализации веб-push.
Это потребует использования нескольких различных API, доступных в сервисном работнике.
Событие закрытия уведомления
В последнем разделе мы увидели, как мы можем прослушивать события notificationclick
.
Существует также событие notificationclose
, которое вызывается, если пользователь отклоняет одно из ваших уведомлений (т. е. вместо того, чтобы щелкнуть уведомление, пользователь щелкает крестик или смахивает уведомление).
Это событие обычно используется для аналитики для отслеживания взаимодействия пользователей с уведомлениями.
self.addEventListener('notificationclose', function (event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
Добавление данных в уведомление
При получении push-сообщения обычно имеются данные, которые полезны только в том случае, если пользователь нажал на уведомление. Например, URL-адрес, который должен открываться при нажатии на уведомление.
Самый простой способ получить данные из push-события и прикрепить их к уведомлению — добавить параметр data
в объект параметров, передаваемый в showNotification()
, например:
const options = {
body:
'This notification has data attached to it that is printed ' +
"to the console when it's clicked.",
tag: 'data-notification',
data: {
time: new Date(Date.now()).toString(),
message: 'Hello, World!',
},
};
registration.showNotification('Notification with Data', options);
Доступ к данным внутри обработчика кликов можно получить с помощью event.notification.data
.
const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
console.log(` ${key}: ${notificationData[key]}`);
});
console.log('');
Открыть окно
Одним из наиболее распространенных ответов на уведомление является открытие окна/вкладки по определенному URL-адресу. Мы можем сделать это с помощью API clients.openWindow()
.
В нашем событии notificationclick
мы запустили такой код:
const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);
В следующем разделе мы рассмотрим, как проверить, открыта ли уже страница, на которую мы хотим направить пользователя, или нет. Таким образом, мы можем сосредоточиться на открытой вкладке, а не открывать новые вкладки.
Фокус на существующем окне
Когда это возможно, нам следует фокусировать окно, а не открывать новое окно каждый раз, когда пользователь щелкает уведомление.
Прежде чем мы рассмотрим, как этого добиться, стоит подчеркнуть, что это возможно только для страниц вашего источника . Это связано с тем, что мы можем видеть только те открытые страницы, которые принадлежат нашему сайту. Это не позволяет разработчикам видеть все сайты, которые просматривают их пользователи.
Взяв предыдущий пример, мы изменим код, чтобы проверить, открыт ли уже /demos/notification-examples/example-page.html
.
const urlToOpen = new URL(examplePage, self.location.origin).href;
const promiseChain = clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
Давайте пройдемся по коду.
Сначала мы анализируем нашу примерную страницу с помощью URL API. Это изящный трюк, который я позаимствовал у Джеффа Посника . Вызов new URL()
с объектом location
вернет абсолютный URL-адрес, если переданная строка является относительной (т. е. /
станет https://example.com/
).
Мы делаем URL-адрес абсолютным, чтобы позже можно было сопоставить его с URL-адресом окна.
const urlToOpen = new URL(examplePage, self.location.origin).href;
Затем мы получаем список объектов WindowClient
, который представляет собой список открытых в данный момент вкладок и окон. (Помните, что это вкладки только для вашего происхождения.)
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
Параметры, переданные в matchAll
сообщают браузеру, что мы хотим искать только клиентов типа «окно» (т. е. просто искать вкладки и окна и исключать веб-работников). includeUncontrolled
позволяет нам искать все вкладки из вашего источника, которые не контролируются текущим сервис-воркером, то есть сервис-воркером, выполняющим этот код. Как правило, при вызове matchAll()
вам всегда нужно, чтобы includeUncontrolled
имел значение true.
Мы фиксируем возвращенное обещание как promiseChain
, чтобы позже передать его в event.waitUntil()
, сохраняя работоспособность нашего сервис-воркера.
Когда обещание matchAll()
разрешается, мы перебираем возвращенные клиенты окна и сравниваем их URL-адреса с URL-адресом, который мы хотим открыть. Если мы найдем совпадение, мы сосредоточим внимание на этом клиенте, что привлечет внимание пользователей к этому окну. Фокусировка осуществляется с помощью вызова matchingClient.focus()
.
Если мы не можем найти подходящего клиента, мы открываем новое окно, как и в предыдущем разделе.
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
Объединение уведомлений
Мы видели, что добавление тега к уведомлению приводит к замене любого существующего уведомления с тем же тегом.
Однако вы можете усложнить свертывание уведомлений с помощью Notifications API. Рассмотрим приложение чата, где разработчик может захотеть, чтобы в новом уведомлении отображалось сообщение, похожее на «У вас есть два сообщения от Мэтта», а не просто отображалось последнее сообщение.
Вы можете сделать это или манипулировать текущими уведомлениями другими способами, используя API Registration.getNotifications() , который дает вам доступ ко всем видимым в данный момент уведомлениям для вашего веб-приложения.
Давайте посмотрим, как мы могли бы использовать этот API для реализации примера чата.
Предположим, что в нашем чат-приложении каждое уведомление содержит некоторые данные, включая имя пользователя.
Первое, что нам нужно сделать, это найти все открытые уведомления для пользователя с определенным именем пользователя. Мы получим registration.getNotifications()
, пройдем по ним и проверим notification.data
для определенного имени пользователя:
const promiseChain = registration.getNotifications().then((notifications) => {
let currentNotification;
for (let i = 0; i < notifications.length; i++) {
if (notifications[i].data && notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
});
Следующим шагом будет замена этого уведомления новым уведомлением.
В этом приложении для поддельных сообщений мы будем отслеживать количество новых сообщений, добавляя счетчик к данным нашего нового уведомления и увеличивая его с каждым новым уведомлением.
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// We have an open notification, let's do something with it.
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
// Remember to close the old notification.
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});
Если в данный момент отображается уведомление, мы увеличиваем количество сообщений и соответствующим образом устанавливаем заголовок и текст уведомления. Если уведомлений нет, мы создаем новое уведомление с newMessageCount
, равным 1.
В результате первое сообщение будет выглядеть так:
Второе уведомление свернёт уведомления следующим образом:
Преимущество этого подхода в том, что если ваш пользователь увидит, что уведомления появляются одно поверх другого, это будет выглядеть более связно, чем просто замена уведомления последним сообщением.
Исключение из правил
Я уже говорил, что вы должны показывать уведомление при получении push-уведомления, и это верно в большинстве случаев. Единственный сценарий, в котором вам не нужно показывать уведомление, — это когда у пользователя открыт и сосредоточен ваш сайт.
Внутри вашего push-события вы можете проверить, нужно ли вам показывать уведомление или нет, исследуя оконные клиенты и ища конкретное окно.
Код для получения всех окон и поиска сфокусированного окна выглядит следующим образом:
function isClientFocused() {
return clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let clientIsFocused = false;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
clientIsFocused = true;
break;
}
}
return clientIsFocused;
});
}
Мы используем clients.matchAll()
для получения всех наших оконных клиентов, а затем перебираем их в цикле, проверяя focused
параметр.
Внутри нашего push-события мы бы использовали эту функцию, чтобы решить, нужно ли нам показывать уведомление:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
console.log("Don't need to show a notification.");
return;
}
// Client isn't focused, we need to show a notification.
return self.registration.showNotification('Had to show a notification.');
});
event.waitUntil(promiseChain);
Отправка сообщения на страницу из push-события
Мы видели, что вы можете пропустить показ уведомления, если пользователь в данный момент находится на вашем сайте. Но что, если вы все равно хотите сообщить пользователю о том, что событие произошло, но уведомление слишком жесткое?
Один из подходов — отправить сообщение от работника службы на страницу. Таким образом, веб-страница может отображать пользователю уведомление или обновление, информируя его о событии. Это полезно в ситуациях, когда тонкое уведомление на странице лучше и удобнее для пользователя.
Допустим, мы получили push-уведомление, проверили, что наше веб-приложение в данный момент сфокусировано, а затем можем «отправить сообщение» на каждую открытую страницу, вот так:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
windowClients.forEach((windowClient) => {
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString(),
});
});
} else {
return self.registration.showNotification('No focused windows', {
body: 'Had to show a notification instead of messaging each page.',
});
}
});
event.waitUntil(promiseChain);
На каждой странице мы прослушиваем сообщения, добавляя прослушиватель событий сообщений:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log('Received a message from service worker: ', event.data);
});
В этом прослушивателе сообщений вы можете делать все, что захотите: отображать собственный пользовательский интерфейс на своей странице или полностью игнорировать сообщение.
Также стоит отметить, что если вы не определите прослушиватель сообщений на своей веб-странице, сообщения от сервис-воркера ничего не сделают.
Кэшировать страницу и открыть окно
Один из сценариев, который выходит за рамки данного руководства, но который стоит обсудить, заключается в том, что вы можете улучшить общий UX вашего веб-приложения, кэшируя веб-страницы, которые, как вы ожидаете, пользователи будут посещать после нажатия на ваше уведомление.
Для этого необходимо, чтобы ваш сервис-воркер был настроен на обработку событий fetch
, но если вы реализуете прослушиватель событий fetch
, убедитесь, что вы используете его в своем событии push
, кэшируя страницу и ресурсы, которые вам понадобятся, прежде чем показывать уведомление.
Совместимость с браузером
Событие notificationclose
Clients.openWindow()
ServiceWorkerRegistration.getNotifications()
clients.matchAll()
Для получения дополнительной информации ознакомьтесь с введением в пост Service Workers .
Куда идти дальше
- Обзор веб-push-уведомлений
- Как работает Push
- Подписка пользователя
- Разрешение UX
- Отправка сообщений с помощью библиотек Web Push
- Веб-пуш-протокол
- Обработка push-событий
- Отображение уведомления
- Поведение уведомлений
- Общие шаблоны уведомлений
- Часто задаваемые вопросы по push-уведомлениям
- Распространенные проблемы и сообщения об ошибках