如何使用通知和推送使 PWA 重新参与

能够缓存应用内容以离线工作是一个很棒的功能。允许用户在设备上安装 Web 应用更棒。但与其仅依赖用户操作,我们可以使用推送消息和通知做更多事情,在有新内容可用时自动重新参与并提供新内容。

两个 API,一个目标

Push APINotifications API 是两个独立的 API,但在您想要在应用中提供引人入胜的功能时,它们可以很好地协同工作。Push 用于在没有任何客户端干预的情况下将新内容从服务器传递到应用,并且其操作由应用的服务工作者处理。通知可以由服务工作者用于向用户显示新信息,或者至少在某些内容更新时提醒他们。

它们在浏览器窗口之外工作,就像服务工作者一样,因此更新可以在应用页面失去焦点甚至关闭时推送,并且通知可以显示。

通知

让我们从通知开始 - 它们可以独立工作,但与推送结合使用时会更有用。让我们首先看一下孤立的通知。

请求权限

要显示通知,我们必须首先请求显示通知的权限。但是,最佳实践建议我们不要立即显示通知,而应该在用户通过单击按钮请求通知时显示弹出窗口。

js
const button = document.getElementById("notifications");
button.addEventListener("click", () => {
  Notification.requestPermission().then((result) => {
    if (result === "granted") {
      randomNotification();
    }
  });
});

这将使用操作系统自己的通知服务显示弹出窗口

Notification of js13kPWA.

当用户确认接收通知时,应用就可以向他们显示通知。用户操作的结果可以是默认、已授予或已拒绝。当用户不做出选择时,将选择默认选项,而当用户分别点击“是”或“否”时,将设置另外两个选项。

当接受后,该权限对通知和推送都有效。

创建通知

示例应用会从可用数据中创建一个通知 - 随机选择一个游戏,并使用选定的游戏为通知提供内容:它将游戏的名称设置为标题,在正文中提到作者,并将图像显示为图标

js
function randomNotification() {
  const randomItem = Math.floor(Math.random() * games.length);
  const notifTitle = games[randomItem].name;
  const notifBody = `Created by ${games[randomItem].author}.`;
  const notifImg = `data/img/${games[randomItem].slug}.jpg`;
  const options = {
    body: notifBody,
    icon: notifImg,
  };
  new Notification(notifTitle, options);
  setTimeout(randomNotification, 30000);
}

每 30 秒就会创建一个新的随机通知,直到它变得过于烦人并被用户禁用。(对于真正的应用,通知的频率应该低得多,并且更有用。)Notifications API 的优势在于它使用了操作系统的通知功能。这意味着即使用户没有查看 Web 应用,也可以向他们显示通知,并且通知看起来与原生应用显示的通知相似。

推送

推送比通知更复杂 - 我们需要订阅一个服务器,然后该服务器会将数据发送回应用。应用的服务工作者将从推送服务器接收数据,然后可以使用通知系统或其他机制(如果需要)显示这些数据。

该技术还处于非常早期的阶段 - 一些工作示例使用 Google Cloud Messaging 平台,但正在被重写以支持 VAPID(自愿应用标识),它为您的应用提供了额外的安全层。您可以检查 Service Workers Cookbook 示例,尝试使用 Firebase 设置推送消息服务器,或构建您自己的服务器(例如使用 Node.js)。

如前所述,要能够接收推送消息,您必须有一个服务工作者,其基础知识已在 使用服务工作者使 PWA 离线工作 文章中进行了说明。在服务工作者内部,通过调用 getSubscription() 方法创建推送服务订阅机制 PushManager 接口。

js
navigator.serviceWorker.register("service-worker.js").then((registration) => {
  return registration.pushManager.getSubscription().then(/* ... */);
});

用户订阅后,他们就可以从服务器接收推送通知。

在服务器端,出于安全原因,整个过程必须使用公钥和私钥加密 - 允许每个人使用您的应用不安全地发送推送消息是一个糟糕的主意。有关保护服务器的详细信息,请参阅 Web Push 数据加密测试页面。服务器存储用户订阅时接收的所有信息,以便以后在需要时发送消息。

要接收推送消息,我们可以在服务工作者文件中监听 push 事件

js
self.addEventListener("push", (e) => {
  /* ... */
});

可以检索数据,然后立即将其显示为用户通知。例如,这可以用于提醒用户某些事情,或让他们知道应用中是否有新内容可用。

推送示例

推送需要服务器部分才能工作,因此我们无法在托管在 GitHub Pages 上的 js13kPWA 示例中包含它,因为它只提供静态文件的托管。所有内容都在 Service Worker Cookbook 中进行了说明 - 请参阅 推送有效负载演示

此演示包含三个文件

  • index.js,其中包含我们应用的源代码
  • server.js,其中包含服务器部分(使用 Node.js 编写)
  • service-worker.js,其中包含服务工作者特定代码。

让我们探索所有这些

index.js

index.js 文件首先注册服务工作者

js
navigator.serviceWorker
  .register("service-worker.js")
  .then((registration) => {
    return registration.pushManager
      .getSubscription()
      .then(async (subscription) => {
        // registration part
      });
  })
  .then((subscription) => {
    // subscription part
  });

它比我们在 js13kPWA 演示 中看到的服务工作者要复杂一些。在这种特殊情况下,注册后,我们使用注册对象进行订阅,然后使用生成的订阅对象完成整个过程。

在注册部分,代码如下所示

js
async (subscription) => {
  if (subscription) {
    return subscription;
  }
};

如果用户已经订阅,我们就会返回订阅对象并继续订阅部分。如果没有,我们就会初始化一个新的订阅

js
const response = await fetch("./vapidPublicKey");
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);

应用会获取服务器的公钥并将响应转换为文本;然后需要将其转换为 Uint8Array(以支持 Chrome)。要了解有关 VAPID 密钥的更多信息,您可以阅读 通过 Mozilla 的推送服务发送 VAPID 标识的 Web 推送通知 博客文章。

应用现在可以使用 PushManager 来订阅新用户。传递给 PushManager.subscribe() 方法有两个选项 - 第一个是 userVisibleOnly: true,这意味着发送给用户的通知都将对他们可见,第二个选项是 applicationServerKey,其中包含我们成功获取和转换的 VAPID 密钥。

js
registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: convertedVapidKey,
});

现在让我们转到订阅部分 - 应用首先使用 Fetch 将订阅详细信息作为 JSON 发送到服务器。

js
fetch("./register", {
  method: "post",
  headers: {
    "Content-type": "application/json",
  },
  body: JSON.stringify({ subscription }),
});

然后在 订阅 按钮上定义了 onclick 函数

js
document.getElementById("doIt").onclick = () => {
  const payload = document.getElementById("notification-payload").value;
  const delay = document.getElementById("notification-delay").value;
  const ttl = document.getElementById("notification-ttl").value;

  fetch("./sendNotification", {
    method: "post",
    headers: {
      "Content-type": "application/json",
    },
    body: JSON.stringify({
      subscription,
      payload,
      delay,
      ttl,
    }),
  });
};

当单击该按钮时,fetch 会请求服务器使用给定的参数发送通知:payload 是在通知中显示的文本,delay 定义了通知显示前的延迟(以秒为单位),ttl 是生存时间设置,它使通知在服务器上可用指定的时间段,也以秒为单位定义。

现在,让我们转到下一个 JavaScript 文件。

server.js

服务器部分使用 Node.js 编写,需要托管在合适的位置,这是一个完全独立的文章的主题。我们这里只提供一个高级概述。

web-push 模块 用于设置 VAPID 密钥,如果 VAPID 密钥尚不可用,则可以选择生成它们。

js
const webPush = require("web-push");

if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
  console.log(
    "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " +
      "environment variables. You can use the following ones:",
  );
  console.log(webPush.generateVAPIDKeys());
  return;
}

webPush.setVapidDetails(
  "https://example.com",
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY,
);

接下来,一个模块定义并导出应用需要处理的所有路由:获取 VAPID 公钥、注册,然后发送通知。您可以看到 index.js 文件中的变量正在使用:payloaddelayttl

js
module.exports = (app, route) => {
  app.get(`${route}vapidPublicKey`, (req, res) => {
    res.send(process.env.VAPID_PUBLIC_KEY);
  });

  app.post(`${route}register`, (req, res) => {
    res.sendStatus(201);
  });

  app.post(`${route}sendNotification`, (req, res) => {
    const subscription = req.body.subscription;
    const payload = req.body.payload;
    const options = {
      TTL: req.body.ttl,
    };

    setTimeout(() => {
      webPush
        .sendNotification(subscription, payload, options)
        .then(() => {
          res.sendStatus(201);
        })
        .catch((error) => {
          console.log(error);
          res.sendStatus(500);
        });
    }, req.body.delay * 1000);
  });
};

service-worker.js

我们将要查看的最后一个文件是 service worker

js
self.addEventListener("push", (event) => {
  const payload = event.data?.text() ?? "no payload";
  event.waitUntil(
    self.registration.showNotification("ServiceWorker Cookbook", {
      body: payload,
    }),
  );
});

它所做的只是为 push 事件添加一个监听器,创建包含从数据中获取的文本的有效载荷变量(如果数据为空,则创建一个要使用的字符串),然后等待通知显示给用户。

如果您想了解这些示例是如何处理的,请随时探索 Service Worker Cookbook 中的其余示例。这里有一大批工作示例,展示了通用用法,以及网络推送、缓存策略、性能、离线工作等等。