《Progressive Web Apps》读书笔记六
第六章 消息推送
6.1 与用户互动 推送通知最大的优点是即使用户没有浏览你的网站也会收到这些通知内容。
要给用户发送推送通知,首先需要用户的授权。
6.2 Weather Channel 天气预报网站 Weather Channel 的数据,略。
6.3 浏览器支持 Web 推送标准:http://www.w3.org/TR/push-api 。
6.4 第一个推送通知 发送推送通知需要三个步骤:
提示用户并获得他们的订阅细节
将这些细节信息保存在服务器上
在需要时发送任何消息
6.4.1 订阅通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 var endpointvar keyvar authSecretvar vapidPublicKey = '...' function urlBase64ToUnit8Array (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 Unit8Array (rawData.length ) for (let i = 0 ; i < rawData.length ; ++i) { outputArray[i] = rawData.charCodeAt (i) } return outputArray } if ('serviceWorker' in navigator) { navigator.serviceWorker .register ('sw.js' ) .then (function (registration ) { return registration.pushManager .getSubscription () .then (function (subscription ) { if (subscription) return return registration.pushManager .subscribe ({ userVisibleOnly : true , applicationServerKey : urlBase64ToUnit8Array (vapidPublicKey) }) .then (function (subscription ) { var rawKey = subscription.getKey ? subscription.getKey ('p256dh' ) : '' key = rawKey ? btoa (String .fromCharCode .apply (null , new Unit8Array (rawKey))) : '' var rawAuthSecret = subscription.getKey ? subscription.getKey ('auth' ) : '' authSecret = rawAuthSecret ? btoa (String .fromCharCode .apply (null , new Unit8Array (rawAuthSecret))) : '' endpoint = subscription.endpoint return fetch ('./register' , { method : 'post' , headers : new Headers ({ 'content-type' : 'application/json' }), body : JSON .stringify ({ endpoint : subscription.endpoint , key : key, authSecret : authSecret }) }) }) }) }).catch (function (err ) { console .log ('serviceWorker registration failed: ' , err) }) }
VAPID(Voluntary Application Server Identification,自主应用服务器标识)协议,本质上定义了应用服务器和推送服务之间的握手,并允许推送服务器确认哪个站点正在发送消息。
成功注册了 Service Worker 之后,可以使用 registration 对象中的 pushManager 来检测用户是否已经订阅过了。如果用户在这台机器上已经订阅过了,便不需要再发送信息给服务器。每个订阅对象包含一个唯一的订阅 ID。
如果用户还没订阅,就用 pushManager.subscribe()
函数来提示用户订阅,该函数使用 VAPID 公钥识别自己。
最后,使用 Fetch API 来发送 POST 请求到服务器上的端点,密钥和 authSecret 将用于存储用户的详细信息。
6.4.2 发送通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const webpush = require ('web-push' ) const express = require ('express' )const bodyParser = require ('body-parser' )const app = express ()webpush.setVapidDetails ( 'mailto:contact@deanhume.com' , 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY' , 'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0' ) app.post ('/register' , function (req, res ) { var endpoint = req.body .endpoint saveRegistrationDetails (endpoint, key, authSecret) const pushSubscription = { endpoint : req.body .endpoint , keys : { auth : req.body .authSecret , p256dh : req.body .key } } var body = 'Thank you for registering' var iconUrl = 'https://example.com/images/homescreen.png' webpush.sendNotification (pushSubscription, JSON .stringify ({ msg : body, url : 'http://localhost: 3111' , icon : iconUrl }) ) .then (result => res.sendStatus (201 )) .catch (err => console .log (err)) }) app.listen (3111 , function ( ) { console .log ('Web push app listening on port 3111!' ) })
6.4.3 接收通知并与之互动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 self.addEventListener ('push' , function (event ) { var payload = event.data ? JSON .parse (event.data .text ()) : 'no payload' var title = 'Progressive Times' event.waitUntil ( self.registration .showNotification (title, { body : payload.msg , url : payload.url , icon : payload.icon }) ) }) self.addEventListener ('click' , function (event ) { event.notification .close () event.waitUntil ( clients.matchAll ({ type : 'window' }) .then (function (clientList ) { for (var i = 0 ; i < clientList.length ; i++) { var client = clientList[i] if (client.url === '/' && 'focus' in client) return client.focus () } if (clients.openWindow ) return clients.openWindow ('http://localhost:3111' ) }) ) }) self.addEventListener ('push' , function (event ) { var payload = event.data ? JSON .parse (event.data .text ()) : 'no payload' var title = 'Progressive Times' event.waitUntil ( self.registration .showNotification (title, { body : payload.msg , url : payload.url , icon : payload.icon , actions : [ { action : 'voteup' , title : 'Vote Up' }, { action : 'votedown' , title : 'Vote Down' } ], vibrate : [300 , 100 , 400 ] }) ) }) self.addEventListener ('click' , function (event ) { event.notification .close () if (event.action === 'voteup' ) { clients.openWindow ('http://localhost:/voteup' ) } else { clients.openWindow ('http://localhost:/votedown' ) } }, false )
6.4.4 取消订阅 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 navigator.serviceWorker .ready .then (serviceWorkerRegistration => { serviceWorkerRegistration.pushManager .getSubscription () .then (subscription => { if (!subscription) return subscription.unsubscribe () .then (function ( ) { console .log ('Successfully unsubscribed!' ) }) .catch (e => { logger.error ('Error thrown while unsubscribing from push messaging' , e) }) }) }) document .getElementById ('unsubscribe' ).addEventListener ('click' , unsubscribe)
6.5 第三方推送通知 如果不想自己搭建推送通知服务器,而是使用 SaaS 产品,有一些现成的第三方解决方案。如 OneSignal、Roost 和 Aimtell 等。