Service Worker学习与实践(三)——消息推送

栏目: Node.js · 发布时间: 6年前

内容简介:在上一篇文章说到底,那么,我们来看一下,如何直接使用

在上一篇文章 Service Worker学习与实践(二)——PWA简介 中,已经讲到 PWA 的起源,优势与劣势,并通过一个简单的例子说明了如何在桌面端和移动端将一个 PWA 安装到桌面上,这篇文章,将通过一个例子阐述如何使用 Service Worker 的消息推送功能,并配合 PWA 技术,带来原生应用般的消息推送体验。

Notification

说到底, PWA 的消息推送也是服务端推送的一种,常见的服务端推送方法,例如广泛使用的轮询、长轮询、 Web Socket 等,说到底,都是客户端与服务端之间的通信,在 Service Worker 中,客户端接收到通知,是基于Notification来进行推送的。

那么,我们来看一下,如何直接使用 Notification 来发送一条推送呢?下面是一段示例代码:

// 在主线程中使用
let notification = new Notification('您有新消息', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

notification.onclick = function() {
  console.log('点击了');
};

复制代码

在控制台敲下上述代码后,则会弹出以下通知:

Service Worker学习与实践(三)——消息推送

然而, Notification 这个 API ,只推荐在 Service Worker 中使用,不推荐在主线程中使用,在 Service Worker 中的使用方法为:

// 添加notificationclick事件监听器,在点击notification时触发
self.addEventListener('notificationclick', function(event) {
  // 关闭当前的弹窗
  event.notification.close();
  // 在新窗口打开页面
  event.waitUntil(
    clients.openWindow('https://google.com')
  );
});

// 触发一条通知
self.registration.showNotification('您有新消息', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

复制代码

读者可以在MDN Web Docs关于 NotificationService Worker 中的相关用法,在本文就不浪费大量篇幅来进行较为详细的阐述了。

申请推送的权限

如果浏览器直接给所有开发者开放向用户推送通知的权限,那么势必用户会受到大量垃圾信息的骚扰,因此这一权限是需要申请的,如果用户禁止了消息推送,开发者是没有权利向用户发起消息推送的。我们可以通过 serviceWorkerRegistration.pushManager.getSubscription 方法查看用户是否已经允许推送通知的权限。修改 sw-register.js 中的代码:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function (swReg) {
    swReg.pushManager.getSubscription()
      .then(function(subscription) {
        if (subscription) {
          console.log(JSON.stringify(subscription));
        } else {
          console.log('没有订阅');
          subscribeUser(swReg);
        }
      });
  });
}
复制代码

上面的代码调用了 swReg.pushManagergetSubscription ,可以知道用户是否已经允许进行消息推送,如果 swReg.pushManager.getSubscriptionPromisereject 了,则表示用户还没有订阅我们的消息,调用 subscribeUser 方法,向用户申请消息推送的权限:

function subscribeUser(swReg) {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  swReg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: applicationServerKey
  })
  .then(function(subscription) {
    console.log(JSON.stringify(subscription));
  })
  .catch(function(err) {
    console.log('订阅失败: ', err);
  });
}
复制代码

上面的代码通过 serviceWorkerRegistration.pushManager.subscribe 向用户发起订阅的权限,这个方法返回一个 Promise ,如果 Promiseresolve ,则表示用户允许应用程序推送消息,反之,如果被 reject ,则表示用户拒绝了应用程序的消息推送。如下图所示:

Service Worker学习与实践(三)——消息推送

serviceWorkerRegistration.pushManager.subscribe 方法通常需要传递两个参数:

  • userVisibleOnly ,这个参数通常被设置为 true ,用来表示后续信息是否展示给用户。
  • applicationServerKey ,这个参数是一个 Uint8Array ,用于加密服务端的推送信息,防止中间人攻击,会话被攻击者篡改。这一参数是由服务端生成的公钥,通过 urlB64ToUint8Array 转换的,这一函数通常是固定的,如下所示:
function urlB64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}
复制代码

关于服务端公钥如何获取,在文章后续会有相关阐述。

处理拒绝的权限

如果在调用 serviceWorkerRegistration.pushManager.subscribe 后,用户拒绝了推送权限,同样也可以在应用程序中,通过 Notification.permission 获取到这一状态, Notification.permission 有以下三个取值,:

  • granted :用户已经明确的授予了显示通知的权限。
  • denied :用户已经明确的拒绝了显示通知的权限。
  • default :用户还未被询问是否授权,在应用程序中,这种情况下权限将视为 denied
if (Notification.permission === 'granted') {
  // 用户允许消息推送
} else {
  // 还不允许消息推送,向用户申请消息推送的权限
}
复制代码

密钥生成

上述代码中的 applicationServerPublicKey 通常情况下是由服务端生成的公钥,在页面初始化的时候就会返回给客户端,服务端会保存每个用户对应的公钥与私钥,以便进行消息推送。

在我的示例演示中,我们可以使用 Google 配套的实验网站web-push-codelab生成公钥与私钥,以便发送消息通知:

Service Worker学习与实践(三)——消息推送

发送推送

Service Worker 中,通过监听 push 事件来处理消息推送:

self.addEventListener('push', function(event) {
  const title = event.data.text();
  const options = {
    body: event.data.text(),
    icon: './images/logo/logo512.png',
  };

  event.waitUntil(self.registration.showNotification(title, options));
});
复制代码

在上面的代码中,在 push 事件回调中,通过 event.data.text() 拿到消息推送的文本,然后调用上面所说的 self.registration.showNotification 来展示消息推送。

服务端发送

那么,如何在服务端识别指定的用户,向其发送对应的消息推送呢?

在调用 swReg.pushManager.subscribe 方法后,如果用户是允许消息推送的,那么该函数返回的 Promise 将会 resolve ,在 then 中获取到对应的 subscription

subscription 一般是下面的格式:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU",
    "auth": "XGWy-wlmrAw3Be818GLZ8Q"
  }
}
复制代码

使用 Google 配套的实验网站web-push-codelab,发送消息推送。

Service Worker学习与实践(三)——消息推送

web-push

在服务端,使用 web-push-libs ,实现公钥与私钥的生成,消息推送功能, Node.js版本

const webpush = require('web-push');

// VAPID keys should only be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();

webpush.setGCMAPIKey('<Your GCM API Key Here>');
webpush.setVapidDetails(
  'mailto:example@yourdomain.org',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

// pushSubscription是前端通过swReg.pushManager.subscribe获取到的subscription
const pushSubscription = {
  endpoint: '.....',
  keys: {
    auth: '.....',
    p256dh: '.....'
  }
};

webpush.sendNotification(pushSubscription, 'Your Push Payload Text');
复制代码

上面的代码中, GCM API Key 需要在Firebase console中申请,申请教程可参考这篇博文。

在这个我写的示例 Demo 中,我把 subscription 写死了:

const webpush = require('web-push');

webpush.setVapidDetails(
  'mailto:503908971@qq.com',
  'BCx1qqSFCJBRGZzPaFa8AbvjxtuJj9zJie_pXom2HI-gisHUUnlAFzrkb-W1_IisYnTcUXHmc5Ie3F58M1uYhZU',
  'g5pubRphHZkMQhvgjdnVvq8_4bs7qmCrlX-zWAJE9u8'
);

const subscription = {
  "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU",
    "auth": "XGWy-wlmrAw3Be818GLZ8Q"
  }
};

webpush.sendNotification(subscription, 'Counterxing');
复制代码

交互响应

默认情况下,推送的消息点击后是没有对应的交互的,配合clients API可以实现一些类似于原生应用的交互,这里参考了这篇博文的实现:

Service Worker 中的 self.clients 对象提供了 Client 的访问, Client 接口表示一个可执行的上下文,如 WorkerSharedWorkerWindow 客户端由更具体的 WindowClient 表示。 你可以从 Clients.matchAll()Clients.get() 等方法获取 Client/WindowClient 对象。

新窗口打开

使用 clients.openWindow 在新窗口打开一个网页:

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  // 新窗口打开
  event.waitUntil(
    clients.openWindow('https://google.com/')
  );
});
复制代码

聚焦已经打开的页面

利用 cilents 提供的相关 API 获取,当前浏览器已经打开的页面 URLs 。不过这些 URLs 只能是和你 SW 同域的。然后,通过匹配 URL ,通过 matchingClient.focus() 进行聚焦。没有的话,则新打开页面即可。

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  const urlToOpen = self.location.origin + '/index.html';

  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);
});
复制代码

检测是否需要推送

如果用户已经停留在当前的网页,那我们可能就不需要推送了,那么针对于这种情况,我们应该怎么检测用户是否正在网页上呢?

通过 windowClient.focused 可以检测到当前的 Client 是否处于聚焦状态。

self.addEventListener('push', function(event) {
  const promiseChain = clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    })
    .then((windowClients) => {
      let mustShowNotification = true;

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i];
        if (windowClient.focused) {
          mustShowNotification = false;
          break;
        }
      }

      return mustShowNotification;
    })
    .then((mustShowNotification) => {
      if (mustShowNotification) {
        const title = event.data.text();
        const options = {
          body: event.data.text(),
          icon: './images/logo/logo512.png',
        };
        return self.registration.showNotification(title, options);
      } else {
        console.log('用户已经聚焦于当前页面,不需要推送。');
      }
    });
});
复制代码

合并消息

该场景的主要针对消息的合并。比如,当只有一条消息时,可以直接推送,那如果该用户又发送一个消息呢? 这时候,比较好的用户体验是直接将推送合并为一个,然后替换即可。 那么,此时我们就需要获得当前已经展示的推送消息,这里主要通过 registration.getNotifications() API 来进行获取。该 API 返回的也是一个 Promise 对象。通过 Promiseresolve 后拿到的 notifications ,判断其 length ,进行消息合并。

self.addEventListener('push', function(event) {
  // ...
    .then((mustShowNotification) => {
      if (mustShowNotification) {
        return registration.getNotifications()
          .then(notifications => {
            let options = {
              icon: './images/logo/logo512.png',
              badge: './images/logo/logo512.png'
            };
            let title = event.data.text();
            if (notifications.length) {
              options.body = `您有${notifications.length}条新消息`;
            } else {
              options.body = event.data.text();
            }
            return self.registration.showNotification(title, options);

          });
      } else {
        console.log('用户已经聚焦于当前页面,不需要推送。');
      }
    });
  // ...
});
复制代码
Service Worker学习与实践(三)——消息推送

小结

本文通过一个简单的例子,讲述了 Service Worker 中消息推送的原理。 Service Worker 中的消息推送是基于 Notification API 的,这一 API 的使用首先需要用户授权,通过在 Service Worker 注册时的 serviceWorkerRegistration.pushManager.subscribe 方法来向用户申请权限,如果用户拒绝了消息推送,应用程序也需要相关处理。

消息推送是基于谷歌云服务的,因此,在国内,收到 GFW 的限制,这一功能的支持并不好, Google 提供了一系列推送相关的库,例如 Node.js 中,使用 web-push 来实现。一般原理是:在服务端生成公钥和私钥,并针对用户将其公钥和私钥存储到服务端,客户端只存储公钥。 Service WorkerswReg.pushManager.subscribe 可以获取到 subscription ,并发送给服务端,服务端利用 subscription 向指定的用户发起消息推送。

消息推送功能可以配合 clients API 做特殊处理。

如果用户安装了 PWA 应用,即使用户关闭了应用程序, Service Worker 也在运行,即使用户未打开应用程序,也会收到消息通知。

在下一篇文章中,我将尝试在我所在的项目中使用 Service Worker ,并通过 WebpackWorkbox 配置来讲述 Service Worker 的最佳实践。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JSP网站开发典型模块与实例精讲

JSP网站开发典型模块与实例精讲

李振捷 / 电子工业出版社 / 2006-8 / 50.0

本书是典型模块与实例精讲丛书中的一本。 本书讲解了使用JSP开发网站系统的经典模块和工程实例,基本囊括了JSP的重点技术,对这些模块稍加修改就可以直接使用到实际项目中。为了方便本书的读者交流在学习中遇到的问题,特地在本书的服务网站上公布了很多QQ群组,读者只要拥有QQ号码,就可以参与到本书的QQ学习群组中一起讨论学习心得。本书的作者还在一定的时间给读者提供在线答疑服务。一起来看看 《JSP网站开发典型模块与实例精讲》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器