PWA 学习笔记之 Push API

in cn-programming •  2 years ago 

Push API 允许 Web 应用程序拥有接收服务器推送消息的能力。对于 Web 应用来说,要能够接收到推送的消息,需要有一个被激活的 service worker。当 service worker 处于激活状态时,我们可以使用 PushManager 实例的 subscribe 方法来订阅推送通知。
Push API 的 PushManager 接口提供了从第三方服务器接收通知以及请求推送通知 URL 的方法,通过 ServiceWorkerRegistration 对象的 pushManager 属性可以轻松地访问到 PushManager 实例。那么如何获取 ServiceWorkerRegistration 对象呢?这个嘛,先看一下以下代码,估计你就懂了:

navigator.serviceWorker.register('service-worker.js')
.then((registration) => {
  console.log(registration);
}, (err) => {
  console.log(err);
});

以上代码运行后,控制台输出的结果如下图所示:

sw-push-manager.png

现在我们已经知道如何访问 PushManager 实例,目前该实例有以下三个方法:

  • getSubscription():该方法返回一个 Promise 对象,若已订阅则返回一个包含现有订阅详细信息的 PushSubscription 对象,若未订阅则返回 null;
  • permissionState():该方法返回一个 Promise 对象,用于获取当前 PushManager 对象的权限状态,可能的值为 'granted''denied''prompt'
  • subscribe():该方法返回一个 Promise 对象,用于订阅推送服务。若订阅成功,则返回一个包含现有订阅详细信息的 PushSubscription 对象。

了解完上面的知识,我们来看一下简单的示例:

  • subscribe():
navigator.serviceWorker
  .register('service-worker.js')
  .then((registration) => {
   // 获取pushManager实例,执行订阅操作
   registration.pushManager.subscribe()
     .then((pushSubscription) => {
        console.log(pushSubscription);
     },(error) => {
        console.log(error);
     });
   }, (err) => {
       console.log(err);
});
  • getSubscription():
registration.pushManager
  .getSubscription()
  .then(function (subscription) {
      // 若尚未订阅则subscription的值为null
     isSubscribed = !(subscription === null);
     if (isSubscribed) {
       console.log('用户已订阅');
     } else {
       console.log('用户未订阅');
     }
});

Service Worker Push Event

在前面的文章中,我们已经介绍过了如何注册 service worker,接下来我们来简单介绍 service worker 如何接收服务端发送的通知消息。Push API 中定义了 push 事件,通过 push 事件我们就能够接收推送服务器发送的消息,具体示例如下:

self.addEventListener('push', function(event) {
  if(event.data) {
    // 假设发送的数据格式为JSON字符串
    var obj = event.data.json();
    console.log(obj);
  }
});

不知道小伙伴是否还记得 PWA 学习笔记之 Web Notifications API 这篇文章中介绍的知识,一般情况下当收到推送通知的时候,我们会立即向用户显示通知消息。在 service worker 工作环境中,我们也可以通过 self.registration 对象的 showNotification 方法,来显示通知消息。

接下来我们来更新一下上面的代码,来实现通知消息的显示,具体代码如下(service-worker.js):

self.addEventListener('push', function (event) {
    if (!(self.Notification && self.Notification.permission === 'granted')) {
        return;
    }
    var data = {};
    if (event.data) {
        data = event.data.json();
    }
    // 解析通知内容
    var title = data.title || "Introducing WhatFontIs is the Easiest Way to Identify Fonts Online";
    var message = data.message || "A free service to help you ...";
    var icon = "https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/10/[email protected]";
    var notification = self.registration.showNotification(title, {
        body: message,
        icon: icon
    });
});

// 处理通知消息的点击事件
self.onnotificationclick = function () {
    if (clients.openWindow) {
        clients.openWindow('https://www.sitepoint.com/finding-fonts-whatfontis/');
    }
};

代码已经更新完了,那么我们应该如何验证上面的功能代码呢?Chrome 的开发大神们还是很给力的,已经考虑到这个需求,早就为我们内置了测试工具。该工具藏在哪里呢?哈哈,估计一些小伙伴早就与它 “邂逅” 了。Are you ready,Follow Me:

  • 首先打开 Chrome 开发者工具
  • 然后选择 Application Tab 页
  • 接着选择左侧 Service Workers 菜单
  • 最后目光聚焦到 Push 标签后的输入框与 Push 按钮

工具找到了,我们立马来实战一下,因为我们预期的数据格式是 JSON,所以我们在 Push 标签后的输入框,输入以下内容:

{"title": "Greeting", "message": "Hello Semlinker"}

输入完成后,点击输入框右侧的 Push 按钮,不出意外的话,你将看到以下通知,具体如下图所示:

push-event-local.jpg

虽然 Push Event 本地已经验证通过了,但实际工作中,我们需求从推送服务器接收通知消息。在介绍如何利用推送服务器发送消息通知前,我们先来了解一下 Webpush 架构:

Webpush Architecture

    +-------+           +--------------+       +-------------+
    |  UA   |           | Push Service |       | Application |
    +-------+           +--------------+       |   Server    |
        |                      |               +-------------+
        |      Subscribe       |                      |
        |--------------------->|                      |
        |       Monitor        |                      |
        |<====================>|                      |
        |                      |                      |
        |          Distribute Push Resource           |
        |-------------------------------------------->|
        |                      |                      |
        :                      :                      :
        |                      |     Push Message     |
        |    Push Message      |<---------------------|
        |<---------------------|                      |
        |                      |                      |

(图形来源:https://tools.ietf.org/html/draft-ietf-webpush-protocol-12)

结合上面的架构图,我们来分析一下主要的消息推送流程:

  • 首先 UA (User-Agent)用户代理,订阅推送服务(Push Service);
  • 当应用服务器产生新的消息时,会把新的消息发送至推送服务器;
  • 推送服务器会根据消息类型,匹配对应的订阅者列表,然后把接收到的新消息推送至每个订阅者(UA)。

是不是感觉看起来挺简单的,实际上真实的开发流程会相对复杂,在实战 Webpush 前,我们先来梳理一下相应的开发流程:

  • 判断当前平台是否支持 Service Worker 和 PushManager,若支持则调用 service worker 对象的 register 方法,注册 service worker;
  • Service Worker 注册成功后,保存 ServiceWorkerRegistration 对象的引用,同时通过该引用对象的pushManager 属性,访问 PushManager 对象,然后利用 PushManager 对象提供的 getSubscription 方法判断当前的订阅状态,若未订阅,则调用 PushManager 对象的 subscribe 方法执行订阅操作;
  • PushManager 对象的 subscribe 方法的参数对象,支持两个属性:
    • userVisibleOnly: 布尔值,表示返回的推送订阅将只能被用于对用户可见的消息。
    • applicationServerKey:推送服务器用来向客户端应用发送消息的公钥。该值是应用程序服务器生成的签名密钥对的一部分,可使用在 P-256 曲线上实现的椭圆曲线数字签名(ECDSA)。可以是DOMStringArrayBuffer
  • 订阅成功后,当 Push Service 接收到新消息时,会根据存储的 endpoint (端点)把新的消息推送到订阅目标。

了解完开发流程,我们开始来实战 Webpush。这里我们的 Push Service 直接使用 web-push-codelab 提供的服务。首先进入 web-push-codelab 页面,复制 Application Server Keys 区域中的 Public Key,该 Key 用于生成执行 subscribe 操作时,所需的 applicationServerKey 参数。

main.js 文件的代码如下:

var swRegistration, isSubscribed;
// 保存 https://web-push-codelab.glitch.me/ 页面中的Public Key,用于生成applicationServerKey
const applicationServerPublicKey = 'BCR82cPVPlEexaFKlozZq-3K7p8ey8WBobzLhfNnsC3IB0yXtof2mzW7n4300SHMeWYSFtSZwtR53HlVCKV25Ow';
// 生成PushManager订阅时所需的applicationServerKey参数值
const applicationServerKey = urlBase64ToUint8Array(applicationServerPublicKey);

// 判断是否支持Service Worker 和 PushManager
if ('serviceWorker' in navigator && 'PushManager' in window) {
    console.log('Service Worker and Push is supported');
    navigator.serviceWorker.register('service-worker.js')
        .then(function (swReg) {
            console.log('Service Worker is registered', swReg);
            swRegistration = swReg;
            checkSubscribed();
        })
        .catch(function (error) {
            console.error('Service Worker Error', error);
        });
} else {
    console.warn('Push messaging is not supported');
}

/**
 * 判断PushManager的订阅状态,若未订阅则执行订阅操作
 */
function checkSubscribed() {
    if (swRegistration) {
        swRegistration.pushManager.getSubscription()
            .then(function (subscription) {
                isSubscribed = !(subscription === null);
                if (isSubscribed) {
                    console.log('User IS subscribed.');
                } else {
                    console.log('User is NOT subscribed.');
                    subscribe();
                }
            });
    }
}

/**
 * 执行订阅操作
 */
function subscribe() {
    swRegistration.pushManager
        .subscribe({
            userVisibleOnly: true,
            applicationServerKey: applicationServerKey
        }).then(function (subscription) {
            console.log('User is subscribed:', subscription);
            console.log(JSON.stringify(subscription));
            isSubscribed = true;
        })
        .catch(function (err) {
            console.log('Failed to subscribe the user: ', err);
        });
}

/**
 * Base64转化为Uint8Array
 * @param base64String
 * @returns {Uint8Array}
 */
function urlBase64ToUint8Array(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;
}

service-worker.js 文件的代码如下:

self.addEventListener('push', function (event) {
    if (!(self.Notification && self.Notification.permission === 'granted')) {
        return;
    }
    var data = {};
    if (event.data) {
        data = event.data.json();
    }
    // 解析通知内容
    var title = data.title || "Introducing WhatFontIs is the Easiest Way to Identify Fonts Online";
    var message = data.message || "A free service to help you ...";
    var icon = "https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/10/[email protected]";
    var notification = self.registration.showNotification(title, {
        body: message,
        icon: icon
    });
});

// 处理通知消息的点击事件
self.onnotificationclick = function () {
    if (clients.openWindow) {
        clients.openWindow('https://www.sitepoint.com/finding-fonts-whatfontis/');
    }
};

以上代码成功运行后,我们需要把 PushManager 订阅成功后返回的 subscription 对象,执行序列化操作,然后把输出的结果复制到 web-push-codelab 页面上 Subscription to Send To 对应的 textarea 输入框中(可以在开发者工具的控制台中复制对应的订阅信息),复制完订阅信息后,我们就可以来测试一下远程的消息推送了,在 web-push-codelab 页面上 Text to Send 对应的 textarea 输入框输入以下测试数据:

{"title": "Greeting - Remote", "message": "Hello Semlinker - Remote"}

最后点击 web-push-codelab 页面上的 SEND PUSH MESSAGE 按钮发送推送消息,不出意外的话,你将看到以下通知,具体如下图所示:

push-event-remote.jpg

本文利用两个示例,简单介绍了 Push API 的相关知识,实际上 Web Push 还会涉及其它的知识,还有挺多值得我们深入研究的。有兴趣的小伙伴可以参考 MDN - Using the Push API向网络应用添加推送通知 这两篇文章。刚开始学习PWA,以上内容有误之处,请小伙伴们多多指教。

参考资源

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!