常见的通知模式

Matt Gaunt

我们将介绍 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);

我们来逐步了解一下代码。

首先,我们使用 网址 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 事件

浏览器支持

  • Chrome:50.
  • Edge:17.
  • Firefox:44.
  • Safari:16。

来源

Clients.openWindow()

浏览器支持

  • Chrome:40.
  • Edge:17.
  • Firefox:44.
  • Safari:11.1。

来源

ServiceWorkerRegistration.getNotifications()

浏览器支持

  • Chrome:40.
  • Edge:17.
  • Firefox:44.
  • Safari:16。

来源

clients.matchAll()

浏览器支持

  • Chrome:42.
  • Edge:17.
  • Firefox:54.
  • Safari:11.1。

来源

如需了解详情,请参阅这篇介绍服务工件的博文

下一步做什么

Codelab