常用通知模式

我们将了解 Web 推送的一些常用实现模式。

这将涉及使用 Service Worker 中提供的一些不同的 API。

通知关闭事件

在上一节中,我们了解了如何监听 notificationclick 事件。

还有一个 notificationclose 事件,如果用户关闭了您的某个通知(即,用户单击了关闭按钮或滑动关闭了通知,而不是单击了通知),则会调用此事件。

此事件通常用于分析,以跟踪用户与通知的互动情况。

self.addEventListener('notificationclose', function (event) {
  const dismissedNotification = event.notification;

  const promiseChain = notificationCloseAnalytics();
  event.waitUntil(promiseChain);
});

向通知添加数据

当收到推送消息时,通常会有一些数据只有在用户点击通知后才有用。例如,单击通知时应打开的网址。

从推送事件中获取数据并将其附加到通知的最简单方法是将 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('');

打开窗口

对通知最常见的响应之一是打开一个窗口/标签页并转到特定网址。我们可以使用 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 那里学到的一个巧妙技巧。使用 location 对象调用 new URL(),如果传入的字符串是相对路径,则会返回绝对网址(即,/ 将变为 https://example.com/)。

我们将网址设为绝对网址,以便稍后将其与窗口网址进行匹配。

const urlToOpen = new URL(examplePage, self.location.origin).href;

然后,我们获取 WindowClient 对象的列表,这是当前打开的标签页和窗口的列表。(请记住,这些只是您来源的标签页。)

const promiseChain = clients.matchAll({
  type: 'window',
  includeUncontrolled: true,
});

传递给 matchAll 的选项告知浏览器我们只想要搜索“window”类型的客户端(即,只查找标签页和窗口,并排除 Web Worker)。includeUncontrolled 允许我们搜索来源中所有未受当前 Service Worker 控制的标签页,即运行此代码的 Service Worker。通常,在调用 matchAll() 时,您始终希望 includeUncontrolled 为 true。

我们将返回的 Promise 捕获为 promiseChain,以便稍后可以将其传递到 event.waitUntil() 中,从而保持 Service Worker 处于活动状态。

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。

结果是第一条消息将如下所示

First notification without merging.

第二条通知会将通知折叠成这样

Second notification with merging.

这种方法的好处是,如果您的用户看到通知一个接一个地出现,它看起来和感觉会比仅仅用最新消息替换通知更具凝聚力。

规则的例外情况

我一直在说,当您收到推送时,您必须显示通知,这在大多数情况下是正确的。您不必显示通知的一种情况是,用户已打开并聚焦您的网站。

在您的推送事件中,您可以通过检查窗口客户端并查找聚焦窗口来检查是否需要显示通知。

获取所有窗口并查找聚焦窗口的代码如下所示

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

从推送事件向页面发送消息

我们已经看到,如果用户当前在您的网站上,您可以跳过显示通知。但是,如果您仍然想让用户知道发生了事件,但通知又过于强硬,该怎么办?

一种方法是从 Service Worker 向页面发送消息,这样网页就可以向用户显示通知或更新,从而告知用户事件的发生。这对于页面中的细微通知更好且对用户更友好的情况非常有用。

假设我们收到了推送,检查到我们的 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);
});

在此消息监听器中,您可以执行任何您想执行的操作,在您的页面上显示自定义 UI 或完全忽略消息。

还值得注意的是,如果您未在网页中定义消息监听器,则来自 Service Worker 的消息将不会执行任何操作。

缓存页面并打开窗口

一个超出本指南范围但值得讨论的场景是,您可以通过缓存您期望用户在点击通知后访问的网页来改善 Web 应用的整体用户体验。

这需要设置您的 Service Worker 来处理 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.

来源

有关更多信息,请查看这篇Service Worker 简介博文

后续步骤

代码实验室