PWA 学习笔记之 Push API
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);
});
以上代码运行后,控制台输出的结果如下图所示:
现在我们已经知道如何访问 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 本地已经验证通过了,但实际工作中,我们需求从推送服务器接收通知消息。在介绍如何利用推送服务器发送消息通知前,我们先来了解一下 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)。可以是
DOMString
或ArrayBuffer
。
- 订阅成功后,当 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 API 的相关知识,实际上 Web Push 还会涉及其它的知识,还有挺多值得我们深入研究的。有兴趣的小伙伴可以参考 MDN - Using the Push API 和 向网络应用添加推送通知 这两篇文章。刚开始学习PWA,以上内容有误之处,请小伙伴们多多指教。