常见的通知模式

Matt Gaunt

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

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

通知关闭事件

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

此外,如果用户关闭您的其中一个应用,则会调用 notificationclose 事件。 通知(即用户没有点击通知,而是点击叉号或滑动 通知)。

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

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

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

向通知添加数据

收到推送消息时,通常只有 在用户已点击通知时有用。例如,网址 用户点击通知时应打开的广告素材。

从推送事件获取数据并将其附加到 通知是向传递的 options 对象添加 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('');

打开窗口

对通知的最常见响应之一是打开 按 Tab 键即可切换到特定网址。我们可以使用 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 解析示例网页。这个技巧是我从小杰那里学到的 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。一般来说,您需要 在调用 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。以一款聊天应用为例,开发者可能希望在该应用中收到新通知, 显示类似于“你收到了两封邮件,来自马特”的邮件而不只是显示 消息。

为此,您可以使用 registration.getNotifications() 通过 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。

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

不合并的首个通知。

第二个通知会将通知收起到以下代码中:

带有合并功能的第二条通知。

这种方法的好处在于,如果您的用户目睹 逐一显示通知,这样看起来 和感觉更连贯 而不是直接用最新消息替换通知

规则的例外情况

我一直说,当您收到推送时,必须显示一条通知, 在大多数情况下都是 true。不必显示通知的情况是 当用户打开您的网站并聚焦于其内容时

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

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

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