常见的通知模式

Matt Gaunt

我们来看看网络推送的一些常见实现模式。

这将涉及使用 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);

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

首先,我们使用 网址 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 工作器)。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 让通知收起更加复杂。假设有一个聊天应用,开发者可能希望新通知显示类似于“You have two messages from Matt”(你有两条来自 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);

通过推送事件向网页发送消息

我们发现,如果用户当前访问您的网站,您可以跳过显示通知的步骤。但是,如果您仍想告知用户发生了某个事件,但通知过于繁琐,该怎么办?

一种方法是从 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);
});

在此消息监听器中,您可以执行任何所需操作、在网页上显示自定义界面或完全忽略该消息。

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

缓存网页并打开窗口

有一种情况没有在本指南的讨论范围之内,但值得讨论一下,那就是您可以缓存预计用户在点击通知后会访问的网页,从而提升 Web 应用的整体用户体验。

这需要设置 Service Worker 来处理 fetch 事件,但如果您实现了 fetch 事件监听器,请务必在显示通知之前缓存所需的页面和资源,以便在 push 事件中利用它。

浏览器兼容性

notificationclose 事件

浏览器支持

  • 50
  • 17
  • 44
  • 16

来源

Clients.openWindow()

浏览器支持

  • 40
  • 17
  • 44
  • 11.1

来源

ServiceWorkerRegistration.getNotifications()

浏览器支持

  • 40
  • 17
  • 44
  • 16

来源

clients.matchAll()

浏览器支持

  • 42
  • 17
  • 54
  • 11.1

来源

如需了解详情,请参阅这篇关于 Service Worker 的博文

下一步做什么

Codelab