揭开 Web 推送通知的神秘面纱

Web 推送如何工作?

简而言之,Web 推送的工作原理是你的应用与浏览器供应商提供的"推送服务"进行交互。为此有三个主要步骤,如下图所示:

创建订阅

在客户端创建 Web 推送订阅,并将该订阅发送到后端。订阅只是一些 JSON 配置,其中包含唯一(特定于浏览器的)客户端标识和一些加密密钥。下面是 Firefox 订阅的示例:

json 复制代码
{
  "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/...",
  "expirationTime": null,
  "keys": {
    "auth": "...",
    "p256dh": "..."
  }
}

在 Safari 中,将收到一个 Apple 端点 ( https://web.push.apple.com/... ),在 Chrome 中,将收到一个 Google 端点 ( https://fcm.googleapis.com/fcm/send/... )。

发送通知

后端使用订阅信息向浏览器供应商托管的推送服务发送推送通知。然后,推送服务确保将其发送回你的浏览器。

处理通知

你的浏览器会收到推送通知,并在 Service Worker 中触发回调。然后,你的 Service Worker 可以选择显示通知或执行你想要执行的任何其他操作。

先决条件:VAPID 密钥

需要 VAPID 密钥才能确保 Web 推送在所有主要浏览器上都能正常工作。VAPID 代表自愿应用程序服务器标识 (VAPID),本质上只是如何生成一组公钥-私钥的规范。尽管有这个名字,但它们实际上并不是免费的,因为 Chrome 和 Safari 都要求提供 VAPID 密钥。我测试过的唯一不需要它们的浏览器是 Firefox。

如果尝试在没有 VAPID 密钥的情况下在 Safari 中订阅推送通知,将收到以下错误:

arduino 复制代码
Subscribing for push requires an applicationServerKey

在Chrome中,将收到以下内容:】

makefile 复制代码
DOMException: Registration failed - missing applicationServerKey, and gcm_sender_id not found in manifest

虽然从技术上讲,你可以自己生成 VAPID 密钥,但使用生成器(如 vapidkeys.com)要容易得多。

服务器端实现

为了从后端应用程序服务器发送 Web 推送通知,必须正确构造、编码和加密消息。根据使用的编程语言,你需要找到一个库来帮助解决这个问题。

如果你使用的是 node.js 后端,那么添加 Web 推送支持非常容易。有一个不错的库,叫做 web-push(www.npmjs.com/package/web...) 。

导入和配置 Web 推送

js 复制代码
import webPush from 'web-push';

要开始使用 web-push ,只需调用该函数来设置 VAPID 密钥。请务必同时包含电子邮件地址(以 mailto: 为前缀)。

js 复制代码
// TODO: Generate VAPID keys (e.g. https://vapidkeys.com/)
const vapid = {
  publicKey: '...',
  privateKey: '...',
};

webPush.setVapidDetails(
  'mailto:<email-address>',
  vapid.publicKey,
  vapid.privateKey
);

商店订阅

Web 推送订阅是在客户端生成的,因此你很可能需要某种方式将订阅从前端传递到后端。然后,还需要以某种方式保存它们,例如将其存储在数据库中或将它们保存在持久化的 JSON 文件中。如果不保存订阅数据,那么当你重新启动服务器时,将丢失所有现有订阅!

js 复制代码
app.post('/subscribe', authenticateRequest, (req, res) => {
  const sub = req.body;
  // TODO: Persist subscription (e.g. to db)
  res.status(200).end();
});

广播通知

唯一需要实现的另一件事是实际创建和发送新通知的方法。如果我们向所有订阅广播通知,那么我们只需循环访问已保存的订阅数组并使用 Web 推送库进行调用 sendNotification 即可。

如果用户撤消了页面上的通知权限(或者订阅已过期),你将收到错误。你可以捕获这些错误并删除无效的订阅。

js 复制代码
async function pushNotification(payload) {
  await Promise.all(subscriptions.map(async (sub) => {
    try {
      await webPush.sendNotification(sub, payload); // throws if not successful
    } catch (err) {
      console.log(sub.endpoint, '->', err.message);
      // TODO: Delete subscription (e.g. from db)
    }
  }));
}

// Test send notification
pushNotification('This is a test notification!');

客户端

客户端实现稍微复杂一些。将需要两个文件:一个用于 Service Worker,一个用于客户端应用程序。我们唯一需要放入服务器工作线程的是处理传入通知的回调。

Service Worker: /sw.js

js 复制代码
self.addEventListener('push', (event) => {
  const options = {
    body: event.data.text(),
    icon: '/apple-touch-icon.png',
    badge: '/badge.png',
  };
  event.waitUntil(self.registration.showNotification('My App', options));
});

Client App: /client.js

在我们的主应用程序脚本中,我们必须负责请求通知权限,注册我们的 Service Worker pushManager(PushManager - Web APIs | MDN (mozilla.org)) ,并使用浏览器的 API 创建推送通知订阅。

请求通知权限

首先,我们需要确保我们有权推送通知(没有这个,我们的通知就毫无意义)。在页面上的某个位置,你可能希望显示一个链接或按钮,用户可以单击该链接或按钮来启用通知。

html 复制代码
<a id="promptLink" onclick="onPromptClick()">Enable notifications</a>

如果通知已被授予(或拒绝),我们可以隐藏链接,或相应地更新 UI。

js 复制代码
function updatePrompt() {
  if ('Notification' in window) {
    if (Notification.permission == 'granted' || Notification.permission == 'denied') {
      promptLink.style.display = 'none';
    } else {
      promptLink.style.display = 'block';
    }
  }
}

function onPromptClick() {
  if ('Notification' in window) {
    Notification.requestPermission().then((permission) => {
      updatePrompt();
      if (permission === 'granted') {
        console.log('Notification permission granted.');
        init();
      } else if (permission === 'denied') {
        console.warn('Notification permission denied.');
      }
    });
  }
}

注册 Service Worker 并启用推送通知

接下来,我们确保 Service Worker 得到支持并注册我们的 Service Worker,以便我们可以接收通知。最后,我们将使用浏览器 pushManager API 请求推送通知,然后将其发送到后端服务器。

对于此步骤,需要 VAPID 公钥。

同时,确保浏览器支持 Service Worker,然后检查是否已经有一个活动的推送通知订阅。否则,创建一个新订阅。

在这两种情况下,我们都会将订阅数据发送到后端,以确保它被存储。

js 复制代码
const vapidPublicKey = '...';

async function initServiceWorker() {
  if ('serviceWorker' in navigator) {
    const swRegistration = await navigator.serviceWorker.register('sw.js');
    const subscription = await swRegistration.pushManager.getSubscription();
    if (subscription) {
      console.log('User is already subscribed:', subscription);
      sendSubscriptionToServer(subscription);
    } else {
      const subscription = await swRegistration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: vapidPublicKey
      });
      console.log('User subscribed:', subscription);
      sendSubscriptionToServer(subscription);
    }
  } else {
    console.warn('Service worker is not supported');
  }
}

function sendSubscriptionToServer(subscription) {
  fetch('/subscribe', {
    method: 'post',
    body: JSON.stringify(subscription),
    headers: { 'content-type': 'application/json' }
  });
}

window.addEventListener('load', () => {
  initServiceWorker();
  updatePrompt();
});

调试:重新加载 Service Worker

请注意,当重新加载页面时,Service Worker 不会自动重新加载。如果在本地工作并对 Service Worker 进行更改,则需要在浏览器的开发工具中手动重新加载 Service Worker,或者可以启用该选项,以便在页面重新加载时自动重新加载 Service Worker!

意外收获:可点击的通知

你可能想要做的另一件事是使通知可点击。最初,我以为单击通知会自动打开关联的页面。然而,事实并非如此。必须在 Service Worker 中自行实现此功能。

实现此目的的代码比我预期的要复杂一些。这是我在网上找到的最好的例子,它确保在单击通知后清除通知,然后打开一个新的浏览器实例/选项卡,或者聚焦现有选项卡(如果它已经打开)。

js 复制代码
const targetUrl = '...';

self.addEventListener('notificationclick', (event) => {
  self.console.log('notificationclick');
  event.notification.close(); // Android needs explicit close.
  event.waitUntil(
    clients.matchAll({type: 'window'}).then( windowClients => {
      // Check if there is already a window/tab open with the target URL
      for (var i = 0; i < windowClients.length; i++) {
        var client = windowClients[i];
        // If so, just focus it.
        if (client.url === targetUrl && 'focus' in client) {
          return client.focus();
        }
      }
      // If not, then open the target URL in a new window/tab.
      if (clients.openWindow) {
        return clients.openWindow(targetUrl);
      }
    })
  );
});

原文:pqvst.com/2023/11/21/...

代码:github.com/pqvst/minim...

相关推荐
鸭梨山大。2 分钟前
NPM组件包 vant部分版本内嵌挖矿代码
前端·安全·npm·node.js·vue
蟾宫曲5 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心5 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
liuxin334455665 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
qq13267029405 小时前
运行Zr.Admin项目(前端)
前端·vue2·zradmin前端·zradmin vue·运行zradmin·vue2版本zradmin
魏时烟6 小时前
css文字折行以及双端对齐实现方式
前端·css
2401_882726487 小时前
低代码配置式组态软件-BY组态
前端·物联网·低代码·前端框架·编辑器·web
web130933203987 小时前
ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
前端·github
胡西风_foxww7 小时前
【ES6复习笔记】迭代器(10)
前端·笔记·迭代器·es6·iterator
前端没钱7 小时前
探索 ES6 基础:开启 JavaScript 新篇章
前端·javascript·es6