在第一篇:介绍一下渐进式 Web App(离线) - Part 1中咱们介绍了一个典型的PWA应该是什么样子的,而且介绍了一下sercer worker和应用壳(app shell),在第二篇[介绍一下渐进式 Web App(即时加载) - Part 2
](https://juejin.im/post/5a3245...,咱们缓存了动态数据,并实现了从本地保存的数据中即时加载数据到页面中,也顺便介绍了web的一些数据库。html
这篇文章也是这系列的完结篇,咱们将实现:前端
push API使Web应用程序可以接收从服务器推送来的消息并通知用户。这个功能须要service worker配合起来,在Web应用程序中典型的推送通知流程的过程是这样子滴:node
push
的监听事件下,能够接收任何传入的消息。咱们先快速总结一下,消息推送时怎么在咱们的web app中实现的git
resources-i-like
仓库在项目中新建js/notification.js
文件,而且在index.html
中引用。github
<script src="./js/notification.js"></script>
js/notification.js 代码以下
(function (window) { 'use strict'; //Push notification button var fabPushElement = document.querySelector('.fab__push'); var fabPushImgElement = document.querySelector('.fab__image'); //To check `push notification` is supported or not function isPushSupported() { //To check `push notification` permission is denied by user if (Notification.permission === 'denied') { alert('User has blocked push notification.'); return; } //Check `push notification` is supported or not if (!('PushManager' in window)) { alert('Sorry, Push notification isn\'t supported in your browser.'); return; } //Get `push notification` subscription //If `serviceWorker` is registered and ready navigator.serviceWorker.ready .then(function (registration) { registration.pushManager.getSubscription() .then(function (subscription) { //If already access granted, enable push button status if (subscription) { changePushStatus(true); } else { changePushStatus(false); } }) .catch(function (error) { console.error('Error occurred while enabling push ', error); }); }); } // Ask User if he/she wants to subscribe to push notifications and then // ..subscribe and send push notification function subscribePush() { navigator.serviceWorker.ready.then(function(registration) { if (!registration.pushManager) { alert('Your browser doesn\'t support push notification.'); return false; } //To subscribe `push notification` from push manager registration.pushManager.subscribe({ userVisibleOnly: true //Always show notification when received }) .then(function (subscription) { toast('Subscribed successfully.'); console.info('Push notification subscribed.'); console.log(subscription); //saveSubscriptionID(subscription); changePushStatus(true); }) .catch(function (error) { changePushStatus(false); console.error('Push notification subscription error: ', error); }); }) } // Unsubscribe the user from push notifications function unsubscribePush() { navigator.serviceWorker.ready .then(function(registration) { //Get `push subscription` registration.pushManager.getSubscription() .then(function (subscription) { //If no `push subscription`, then return if(!subscription) { alert('Unable to unregister push notification.'); return; } //Unsubscribe `push notification` subscription.unsubscribe() .then(function () { toast('Unsubscribed successfully.'); console.info('Push notification unsubscribed.'); console.log(subscription); //deleteSubscriptionID(subscription); changePushStatus(false); }) .catch(function (error) { console.error(error); }); }) .catch(function (error) { console.error('Failed to unsubscribe push notification.'); }); }) } //To change status function changePushStatus(status) { fabPushElement.dataset.checked = status; fabPushElement.checked = status; if (status) { fabPushElement.classList.add('active'); fabPushImgElement.src = '../images/push-on.png'; } else { fabPushElement.classList.remove('active'); fabPushImgElement.src = '../images/push-off.png'; } } //Click event for subscribe push fabPushElement.addEventListener('click', function () { var isSubscribed = (fabPushElement.dataset.checked === 'true'); if (isSubscribed) { unsubscribePush(); } else { subscribePush(); } }); isPushSupported(); //Check for push notification support })(window);
上面的代码作了不少事情。放心啦,我将会解释一波代码的功能滴。web
//Push notification button var fabPushElement = document.querySelector('.fab__push'); var fabPushImgElement = document.querySelector('.fab__image');
上面的代码获取推送通知激活和停用按钮的节点。mongodb
function isPushSupported() { //To check `push notification` permission is denied by user if (Notification.permission === 'denied') { alert('User has blocked push notification.'); return; } //Check `push notification` is supported or not if (!('PushManager' in window)) { alert('Sorry, Push notification isn\'t supported in your browser.'); return; } //Get `push notification` subscription //If `serviceWorker` is registered and ready navigator.serviceWorker.ready .then(function (registration) { registration.pushManager.getSubscription() .then(function (subscription) { //If already access granted, enable push button status if (subscription) { changePushStatus(true); } else { changePushStatus(false); } }) .catch(function (error) { console.error('Error occurred while enabling push ', error); }); }); }
上面的代码是检查浏览器以是否支持推送通知。如今,最重要的是 service worker 必须注册而且在您尝试订阅用户以接收推送通知以前,已经作好了准备(ready)。所以,上面的代码也检查service worker是否ready
并得到用户的订阅。shell
//To change status function changePushStatus(status) { fabPushElement.dataset.checked = status; fabPushElement.checked = status; if (status) { fabPushElement.classList.add('active'); fabPushImgElement.src = '../images/push-on.png'; } else { fabPushElement.classList.remove('active'); fabPushImgElement.src = '../images/push-off.png'; } }
用户订阅按钮的样式改变数据库
changePushStatus
函数表明着只需更改按钮的颜色来指示用户是否已订阅。json
// Ask User if he/she wants to subscribe to push notifications and then // ..subscribe and send push notification function subscribePush() { navigator.serviceWorker.ready.then(function(registration) { if (!registration.pushManager) { alert('Your browser doesn\'t support push notification.'); return false; } //To subscribe `push notification` from push manager registration.pushManager.subscribe({ userVisibleOnly: true //Always show notification when received }) .then(function (subscription) { toast('Subscribed successfully.'); console.info('Push notification subscribed.'); console.log(subscription); //saveSubscriptionID(subscription); changePushStatus(true); }) .catch(function (error) { changePushStatus(false); console.error('Push notification subscription error: ', error); }); }) }
上面的代码负责弹出请求用户是否容许或阻止浏览器中的推送消息。若是用户容许推送消息,就是弹出一个toast
的已经容许的提示,而后更改按钮的颜色并保存订阅ID。若是推浏览器不支持,那么它会通知用户它不受支持。
注意:保存订阅ID的功能如今已被注释掉。
// Unsubscribe the user from push notifications function unsubscribePush() { navigator.serviceWorker.ready .then(function(registration) { //Get `push subscription` registration.pushManager.getSubscription() .then(function (subscription) { //If no `push subscription`, then return if(!subscription) { alert('Unable to unregister push notification.'); return; } //Unsubscribe `push notification` subscription.unsubscribe() .then(function () { toast('Unsubscribed successfully.'); console.info('Push notification unsubscribed.'); //deleteSubscriptionID(subscription); changePushStatus(false); }) .catch(function (error) { console.error(error); }); }) .catch(function (error) { console.error('Failed to unsubscribe push notification.'); }); }) }
上面的是负责退订推送消息,弹出一个toast
提示当心,而后更改按钮的颜色并删除订阅ID。
注意:删除订阅ID的功能如今已被注释掉了。
//Click event for subscribe push fabPushElement.addEventListener('click', function () { var isSubscribed = (fabPushElement.dataset.checked === 'true'); if (isSubscribed) { unsubscribePush(); } else { subscribePush(); } });
上面代码是添加一个按钮单击事件实现订阅和取消订阅用户的切换。
咱们已经可以看到推送订阅了。如今,咱们须要可以保存每一个用户的订阅ID,当用户退订的推送通知时咱们还须要可以删除这些订阅ID。
添加下面的代码到你的js/notification.js
中
function saveSubscriptionID(subscription) { var subscription_id = subscription.endpoint.split('gcm/send/')[1]; console.log("Subscription ID", subscription_id); fetch('http://localhost:3333/api/users', { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id : subscription_id }) }); } function deleteSubscriptionID(subscription) { var subscription_id = subscription.endpoint.split('gcm/send/')[1]; fetch('http://localhost:3333/api/user/' + subscription_id, { method: 'delete', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); }
在上面的代码中,咱们从服务器请求一个接口,来获取订阅ID和和删除订阅ID,saveSubscriptionID
函数建立了一个新的用户而且保存了用户的订阅ID,deleteSubscriptionID
删除了用户和用户的订阅ID
看起来怪怪的。为何要请求到服务器?简单,由于咱们须要一个数据库来存储全部的订阅ID,这样子就能够向全部的用户发送消息推送。
这个API Service 处理了保存和删除订阅ID同时也处理了消息推送的功能。这API的分解。它将有3个api路由:
POST /api/users
建立新用户并存储其订阅IDDELETE /api/user/:user_id
删除和取消订阅用户POST /api/notify
向全部订阅用户发送通知很高兴,我有API Service的源码,道友能够点击连接查看,运行时候确保你的node
和mongodb
是事先安装过的。克隆xi下来而且在命令行中运行node server.js
确保你先建立.env
文件,以下图所示
舒适提醒:您能够经过这个良好的教程去了解如何设置API服务。在本教程我只是实现了node.js版本的API服务。
咱们将使用Firebase Cloud Messaging 做为咱们的消息服务。因此,如今用Firebase去创建一个新的项目。新建项目完了以后就去Project settings > Cloud Messaging
这里
拿到你的 Server Key
而后复制粘贴到你的.env
文件在的FCM_API_KEY
,经过咱们的API Server
咱们须要将Server Key
传给Firebase
,下面看看咱们的看看咱们的消息推送控制器的代码:
.... notifyUsers: function(req, res){ var sender = new gcm.Sender(secrets.fcm); // Prepare a message to be sent var message = new gcm.Message({ notification: { title: "New commit on Github Repo: RIL", icon: "ic_launcher", body: "Click to see the latest commit'" } }); User.find({}, function(err, users) { // user subscription ids to deliver message to var user_ids = _.map(users, 'user_id'); console.log("User Ids", user_ids); // Actually send the message sender.send(message, { registrationTokens: user_ids }, function (err, response) { if (err) { console.error(err); } else { return res.json(response); } }); }); }, .....
如今返回咱们的js/notification.js
而且去掉咱们以前说的saveSubscriptionID
函数和deleteSubscriptionID
的注释,而后你的notification.js
应该是长这样子滴:
(function (window) { 'use strict'; //Push notification button var fabPushElement = document.querySelector('.fab__push'); var fabPushImgElement = document.querySelector('.fab__image'); //To check `push notification` is supported or not function isPushSupported() { //To check `push notification` permission is denied by user if (Notification.permission === 'denied') { alert('User has blocked push notification.'); return; } //Check `push notification` is supported or not if (!('PushManager' in window)) { alert('Sorry, Push notification isn\'t supported in your browser.'); return; } //Get `push notification` subscription //If `serviceWorker` is registered and ready navigator.serviceWorker.ready .then(function (registration) { registration.pushManager.getSubscription() .then(function (subscription) { //If already access granted, enable push button status if (subscription) { changePushStatus(true); } else { changePushStatus(false); } }) .catch(function (error) { console.error('Error occurred while enabling push ', error); }); }); } // Ask User if he/she wants to subscribe to push notifications and then // ..subscribe and send push notification function subscribePush() { navigator.serviceWorker.ready.then(function(registration) { if (!registration.pushManager) { alert('Your browser doesn\'t support push notification.'); return false; } //To subscribe `push notification` from push manager registration.pushManager.subscribe({ userVisibleOnly: true //Always show notification when received }) .then(function (subscription) { toast('Subscribed successfully.'); console.info('Push notification subscribed.'); console.log(subscription); saveSubscriptionID(subscription); changePushStatus(true); }) .catch(function (error) { changePushStatus(false); console.error('Push notification subscription error: ', error); }); }) } // Unsubscribe the user from push notifications function unsubscribePush() { navigator.serviceWorker.ready .then(function(registration) { //Get `push subscription` registration.pushManager.getSubscription() .then(function (subscription) { //If no `push subscription`, then return if(!subscription) { alert('Unable to unregister push notification.'); return; } //Unsubscribe `push notification` subscription.unsubscribe() .then(function () { toast('Unsubscribed successfully.'); console.info('Push notification unsubscribed.'); console.log(subscription); deleteSubscriptionID(subscription); changePushStatus(false); }) .catch(function (error) { console.error(error); }); }) .catch(function (error) { console.error('Failed to unsubscribe push notification.'); }); }) } //To change status function changePushStatus(status) { fabPushElement.dataset.checked = status; fabPushElement.checked = status; if (status) { fabPushElement.classList.add('active'); fabPushImgElement.src = '../images/push-on.png'; } else { fabPushElement.classList.remove('active'); fabPushImgElement.src = '../images/push-off.png'; } } //Click event for subscribe push fabPushElement.addEventListener('click', function () { var isSubscribed = (fabPushElement.dataset.checked === 'true'); if (isSubscribed) { unsubscribePush(); } else { subscribePush(); } }); function saveSubscriptionID(subscription) { var subscription_id = subscription.endpoint.split('gcm/send/')[1]; console.log("Subscription ID", subscription_id); fetch('http://localhost:3333/api/users', { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id : subscription_id }) }); } function deleteSubscriptionID(subscription) { var subscription_id = subscription.endpoint.split('gcm/send/')[1]; fetch('http://localhost:3333/api/user/' + subscription_id, { method: 'delete', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); } isPushSupported(); //Check for push notification support })(window);
让咱们尝试激活消息推送,看看是否建立了新的用户,并存储在咱们的API服务数据库中。从新刷新网页并按下激活按钮,而后看到控制台竟然有错误。
别烦恼!着缘由是咱们没有在咱们的程序中创一个manifest.json
的文件。
如今有趣的事情是,添加上了manifest.json
的文件将不会报错而且在咱们的程序中添加了一个新的功能。有了这个manifest.json
的文件,咱们能够将咱们的应用程序安装到咱们的屏幕上。Viola!!!
如今咱们去建立一个manifest.json
的文件吧,代码以下
{ "name": "PWA - Commits", "short_name": "PWA", "description": "Progressive Web Apps for Resources I like", "start_url": "./index.html?utm=homescreen", "display": "standalone", "orientation": "portrait", "background_color": "#f5f5f5", "theme_color": "#f5f5f5", "icons": [ { "src": "./images/192x192.png", "type": "image/png", "sizes": "192x192" }, { "src": "./images/168x168.png", "type": "image/png", "sizes": "168x168" }, { "src": "./images/144x144.png", "type": "image/png", "sizes": "144x144" }, { "src": "./images/96x96.png", "type": "image/png", "sizes": "96x96" }, { "src": "./images/72x72.png", "type": "image/png", "sizes": "72x72" }, { "src": "./images/48x48.png", "type": "image/png", "sizes": "48x48" } ], "author": { "name": "Prosper Otemuyiwa", "website": "https://twitter.com/unicodeveloper", "github": "https://github.com/unicodeveloper", "source-repo": "https://github.com/unicodeveloper/pwa-commits" }, "gcm_sender_id": "571712848651" }
如今快速的扫盲一下manifest.json
上的key
的含义吧。
fullscreen
, standalone
, minimal-ui
Firebase
的sender_id
,在下面取得。以下图在你的index.html
和latest.html
引用这个manifest.json
文件。
<link rel="manifest" href="./manifest.json">
如今,清楚缓存,刷新应用,而后点击消息推进按钮
而后看到 订阅ID在控制台中打印了出来,查看下出数据库,
Yaaay!!,中于起做用了呢
在数据库中你能够看到用户的订阅ID了,这意味着,咱们的请求是成功滴
小提示:
RoboMongo是一个管理
mongodb
数据库的图形界面。
您能够尝试取消订阅,查看它如何从API服务数据库中删除用户。
在咱们的service API
中,咱们作一个POST请求到/api/notify
的路由,而后后台接收到前端的请求继续推送到Firebase Cloud Messaging
的服务中。如今,这仍是不够滴,因此,咱们还须要一种在浏览器中监听和接受此通知的方法。
而后到Service Worker
闪亮登场了,用它来监听一个push
的事件,在sw.js
中,代码以下:
self.addEventListener('push', function(event) { console.info('Event: Push'); var title = 'New commit on Github Repo: RIL'; var body = { 'body': 'Click to see the latest commit', 'tag': 'pwa', 'icon': './images/48x48.png' }; event.waitUntil( self.registration.showNotification(title, body) ); });
这段代码添加到sw.js
。清缓存,从新加载你的应用,如今咱们利用postman去发起http://localhost:3333/api/notify
请求
当发出通知时,咱们的浏览器会欢迎这样的通知:
接到通知后,当用户单击这个通知时,咱们能够决定该怎么作。而后添加下面这个代码到sw.js
中
self.addEventListener('notificationclick', function(event) { var url = './latest.html'; event.notification.close(); //Close the notification // Open the app and navigate to latest.html after clicking the notification event.waitUntil( clients.openWindow(url) ); });
这里,上面的代码监听用户单击通知时被触发的事件。event.notification.close()
是单击后关闭通知。而后,将打开一个浏览器新窗口或选项卡从新指向localhost:8080/latest.html
地址。
提示:event.waitUntil()
在咱们的新窗口打开以前,它就被调用了以确保浏览器不会终止咱们的server worker
。
以前咱们是经过Postman
手动发起一个请求去推送消息的,实际上,咱们是想,用于一旦有了提交到https://github.com/unicodeveloper/resources-i-like/
,咱们就自动接收收到一个消息通知。那么,咱们如何使这个过程自动化呢?
有据说过Webhooks
么???
有!!!
那么好~~咱们就用GitHub Webhooks
提示: 使用您本身的仓库地址,由于看到了这里,你就要本身提交commit了
到你选择的仓库去,在这里个人是https://github.com/unicodeveloper/resources-i-like/
,到 Settings > Webhooks
中:
点击add webhook
添加一个hook,当用户提交的时候就触发了pushs
是事件,这个hook讲通知咱们的notify API
,利用这个webhook当用户提交commit时候,发出一个post请求到咱们的/api/notify
,而后顺利成章的发送一个浏览器的消息推送啦。 开森~~~~
看上面那个图,慢着,等一下,是怎么获得https://ea71f5aa.ngrok.io/api/notify
z这个地址的??其实是在本地开发须要用ngrok工具,把内网转发出去。明白了吧
很是简单,咱们不能使用localhost
,GitHub上须要一个存在在网络上URL,我利用ngrok能够将本地服务器暴露到Internet上。
安装ngrok以后,在命令行中,敲上这样子的代码
./ngrok http 3333
获得
提示:ngrok输出HTTP和HTTPS地址,它们都映射到本地服务。
如今,一旦你添加webhook,GitHub当即提交测试post
请求来决定是否设置正确。
咱们把一切都作好了,如今咱们去提交一个commit,一旦你这么作了,一个消息推送就发送到咱们的浏览器中了。 以下图
一个PWA的要求服务是经过HTTPS的。用Firebase hosting部署咱们的应用程序服务器而且支持HTTPS协议是一个很是好的选择。
咱们app线上的地址:https://ril-pwa.firebaseapp.com/
服务器api线上地址:https://rilapi.herokuapp.com/api
打开你的设备上的浏览器,尤为是Chrome,并像这样添加它:
看到桌面上已经有了 应用图标了。而后本教程也是总结鸟。
~ 完结,散花散花散花散花 ~~~~
附:
点击连接查看
第一篇: 介绍一下渐进式 Web App(离线) - Part 1
第二篇: 介绍一下渐进式 Web App(即时加载)- Part 2
若是有那个地方翻译出错或者失误,请各位大神不吝赐教,小弟感激涕零