常见的通知模式

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);

我们来单步调试一下代码。

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

浏览器支持

  • 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