博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Service Worker学习与实践(三)——消息推送
阅读量:6247 次
发布时间:2019-06-22

本文共 9292 字,大约阅读时间需要 30 分钟。

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

Notification

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

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

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

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

然而,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',});

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

申请推送的权限

如果浏览器直接给所有开发者开放向用户推送通知的权限,那么势必用户会受到大量垃圾信息的骚扰,因此这一权限是需要申请的,如果用户禁止了消息推送,开发者是没有权利向用户发起消息推送的。我们可以通过方法查看用户是否已经允许推送通知的权限。修改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);  });}

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

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有以下三个取值,:

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

密钥生成

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

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

发送推送

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

在服务端,使用,实现公钥与私钥的生成,消息推送功能,。

const webpush = require('web-push');// VAPID keys should only be generated only once.const vapidKeys = webpush.generateVAPIDKeys();webpush.setGCMAPIKey('
');webpush.setVapidDetails( 'mailto:example@yourdomain.org', vapidKeys.publicKey, vapidKeys.privateKey);// pushSubscription是前端通过swReg.pushManager.subscribe获取到的subscriptionconst pushSubscription = { endpoint: '.....', keys: { auth: '.....', p256dh: '.....' }};webpush.sendNotification(pushSubscription, 'Your Push Payload Text');

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

在这个我写的示例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');

交互响应

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

Service Worker中的
self.clients对象提供了
Client的访问,
Client接口表示一个可执行的上下文,如
Worker
SharedWorker
Window客户端由更具体的
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中的消息推送是基于Notification API的,这一API的使用首先需要用户授权,通过在Service Worker注册时的serviceWorkerRegistration.pushManager.subscribe方法来向用户申请权限,如果用户拒绝了消息推送,应用程序也需要相关处理。

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

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

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

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

转载地址:http://wnlia.baihongyu.com/

你可能感兴趣的文章
tr命令练习
查看>>
LNMP部署实例及HTTPS服务实现
查看>>
9种用户体验设计的状态是必须知道的(四)
查看>>
什么是DVB-SI?对PSI(PAT,PMT,CAT,NIT,SDT,EIT)的理解
查看>>
JavaSE 学习参考:方法重写
查看>>
Percona MySQL 5.7 Linux通用二进制tar包安装(CentOS 6.5)
查看>>
90后女生吴江平独闯9个国家 吴江平穷游照片欣赏
查看>>
linux密码策略
查看>>
【REACT NATIVE 跨平台应用开发】环境搭建问题记录&&XCODE7模拟器上COMMAND+R失效的几种替换方法...
查看>>
C++实现选择排序
查看>>
面试题:合并两个排序的链表
查看>>
PPT控件 Spire.Presentation for .NET V2.8.35发布 | 支持设置演示幻灯片布局
查看>>
云环境所面临的安全威胁
查看>>
STM32 USB转串口驱动移植到SylixOS中遇到的问题总结
查看>>
组播学习分享 第三天
查看>>
【C#小知识】C#中一些易混淆概念总结(五)---------深入解析C#继承
查看>>
数据库优化
查看>>
TensorFlow的基本运算01-03
查看>>
Hive-有意思的query
查看>>
SylixOS调试与性能分析技术--内存泄漏检测
查看>>