PWA 学习笔记之 Push API

in #cn-programming7 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,以上内容有误之处,请小伙伴们多多指教。

参考资源