我们将了解 Web 推送的一些常见实现模式。
这需要使用 Service Worker 中提供的一些不同的 API。
通知关闭事件
在上一部分中,我们了解了如何监听 notificationclick
事件。
此外,如果用户关闭您的某条通知(即用户点击“X”或滑动关闭通知,而不是点击通知),系统还会调用 notificationclose
事件。
此事件通常用于分析,以跟踪用户与通知的互动情况。
self.addEventListener('notificationclose', function (event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
向通知添加数据
收到推送消息时,通常会有一些数据只有在用户点击了通知后才有用。例如,在用户点击通知后应打开的网址。
如需从推送事件中获取数据并将其附加到通知,最简单的方法是将 data
参数添加到传递给 showNotification()
的 options 对象,如下所示:
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('');
打开窗口
对通知最常见的响应之一是打开一个窗口/标签页,以显示特定网址。我们可以使用 clients.openWindow()
API 来执行此操作。
在 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 解析示例网页。这是我从 Jeff Posnick 那里学到的一条小技巧。如果传入的字符串是相对字符串(即 /
将变为 https://example.com/
),使用 location
对象调用 new URL()
将返回绝对网址。
我们将网址设为绝对网址,以便稍后将其与窗口网址进行匹配。
const urlToOpen = new URL(examplePage, self.location.origin).href;
然后,我们会获取 WindowClient
对象的列表,即当前打开的标签页和窗口的列表。(请注意,这些标签页仅供您查看。)
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
传递给 matchAll
的选项会告知浏览器,我们只想搜索“窗口”类型的客户端(即仅查找标签页和窗口,并排除 Web Worker)。借助 includeUncontrolled
,我们可以搜索您的源中不受当前服务工作线程(即运行此代码的服务工作线程)控制的所有标签页。一般而言,调用 matchAll()
时,您总是希望 includeUncontrolled
为 true。
我们将返回的 promise 捕获为 promiseChain
,以便稍后将其传入 event.waitUntil()
,从而使服务工作器保持活跃状态。
matchAll()
promise 解析后,我们会迭代返回的窗口客户端,并将其网址与我们要打开的网址进行比较。如果找到匹配项,我们会将焦点放在该客户端上,以便用户注意到该窗口。聚焦是通过 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 更精细地收起通知。假设有一个聊天应用,在该应用中,开发者可能希望新通知显示类似于“您有两条来自 Matt 的消息”的消息,而不是仅显示最新消息。
为此,您可以使用 registration.getNotifications() API 以其他方式操纵当前通知,该 API 可让您访问您的 Web 应用当前显示的所有通知。
我们来看看如何使用此 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 的新通知。
结果是第一条消息将如下所示:
如果收到第二条通知,系统会将通知收起,如下所示:
这种方法的好处在于,如果用户看到通知逐一显示,则看起来会更协调,而不是仅用最新消息替换通知。
规则的例外情况
我一直在说,您必须在收到推送时显示通知,这在大多数情况下是正确的。在用户打开并专注于您的网站时,您无需显示通知。
在推送事件内,您可以通过检查窗口客户端并查找聚焦的窗口,来确认是否需要显示通知。
用于获取所有窗口并查找聚焦窗口的代码如下所示:
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
参数。
在推送事件中,我们将使用此函数来确定是否需要显示通知:
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);
通过推送事件向网页发送消息
我们了解到,如果用户目前在您的网站上,您可以跳过显示通知。但是,如果您仍希望让用户知道某个事件已发生,但通知过于粗暴,该怎么办?
一种方法是从服务工件向网页发送消息,这样网页就可以向用户显示通知或更新,告知他们发生了事件。在页面中显示不太显眼的通知对用户来说更友好、更实用时,这种方式非常有用。
假设我们收到了推送,并检查了我们的 Web 应用目前是否处于聚焦状态,然后我们就可以向每个打开的网页“发布消息”,如下所示:
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);
});
在此消息监听器中,您可以执行所需的任何操作,在网页上显示自定义界面或完全忽略该消息。
另请注意,如果您未在网页中定义消息监听器,则来自 Service Worker 的消息将不会执行任何操作。
缓存网页并打开窗口
有一种情况不在本指南的讨论范围内,但值得探讨的是,您可以通过缓存您希望用户在点击通知后访问的网页,来提升 Web 应用的整体用户体验。
这需要将您的服务工件设置为处理 fetch
事件,但如果您实现了 fetch
事件监听器,请务必在展示通知之前缓存所需的页面和资源,以便在 push
事件中使用该监听器。
浏览器兼容性
notificationclose
事件
Clients.openWindow()
ServiceWorkerRegistration.getNotifications()
clients.matchAll()
如需了解详情,请参阅这篇介绍服务工件的博文。
下一步做什么
- 网络推送通知概览
- 推送通知的运作方式
- 为用户订阅
- 权限用户体验
- 使用 Web 推送库发送消息
- Web 推送协议
- 处理推送事件
- 显示通知
- 通知行为
- 常见通知模式
- 推送通知常见问题解答
- 常见问题和报告错误