摘要:在的的监听事件下,可以接收任何传入的消息。这个也将负责向已激活推送消息的所有用户发送消息。上面的代码负责弹出请求用户是否允许或阻止浏览器中的推送消息。这个处理了保存和删除订阅同时也处理了消息推送的功能。我们将使用作为我们的消息服务。
在第一篇:介绍一下渐进式 Web App(离线) - Part 1中我们介绍了一个典型的PWA应该是什么样子的,并且介绍了一下sercer worker和应用壳(app shell),在第二篇[介绍一下渐进式 Web App(即时加载) - Part 2
](https://juejin.im/post/5a3245...,我们缓存了动态数据,并实现了从本地保存的数据中即时加载数据到页面中,也顺便介绍了web的一些数据库。
这篇文章也是这系列的完结篇,我们将实现:
在Web应用程序中激活推送通知
使Web应用程序的变成是可以可安装的
消息推送push API使Web应用程序能够接收从服务器推送来的消息并通知用户。这个功能需要service worker配合起来,在Web应用程序中典型的推送通知流程的过程是这样子滴:
Web应用程序弹出一个弹出窗口,要求用户订阅通知。
用户订阅接收推送通知。
service worker 负责管理、处理用户的订阅。
当服务器推送消息时都会用到用户订阅的ID,每个用户根据他们的订阅ID都可以有一个自定义的功能。
在service worker的push的监听事件下,可以接收任何传入的消息。
让我们开始吧。我们先快速总结一下,消息推送时怎么在我们的web app中实现的
给用户一个开关,单击一个按钮激活或停用推送通知。
如果用户激活,请订阅用户通过service worker的管理接收推送通知。
构建一个API 去处理用户删除或者保存订阅ID。这个API也将负责向已激活推送消息的所有用户发送消息。
建立一个GitHub Webhook去实现立即发送通知,让新的提交被推送到resources-i-like仓库
代码行动起来在项目中新建js/notification.js文件,并且在index.html中引用。
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);
上面的代码做了很多事情。放心啦,我将会解释一波代码的功能滴。
//Push notification button var fabPushElement = document.querySelector(".fab__push"); var fabPushImgElement = document.querySelector(".fab__image");
上面的代码获取推送通知激活和停用按钮的节点。
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并获得用户的订阅。
//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函数代表着只需更改按钮的颜色来指示用户是否已订阅。
// 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,当用户退订的推送通知时我们还需要能够删除这些订阅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这个API Service 处理了保存和删除订阅ID同时也处理了消息推送的功能。这API的分解。它将有3个api路由:
POST /api/users创建新用户并存储其订阅ID
DELETE /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的含义吧。
name:表示应用程序的名称,因为它通常显示在屏幕上给用户看滴。
short_name:表示Web应用程序名称的缩写。
description:表示Web应用程序的一般描述。
start_url:是用户启动Web应用程序时加载的URL。
display:定义Web应用程序的默认显示模式。不同的模式有fullscreen, standalone, minimal-ui
orientation:
background_color:表示Web应用程序的背景颜色。
theme_color:表示应用程序的默认主题颜色。它将Android上的状态栏着色。
icons:主屏幕的icon图标
author:作者的一些信息
gcm_sender_id:用于识别应用的Firebase的sender_id,在下面取得。如下图
在你的index.html 和latest.html引用这个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/notifyz这个地址的??实际上是在本地开发需要用ngrok工具,把内网转发出去。明白了吧
设置Ngrok非常简单,我们不能使用localhost,GitHub上需要一个存在在网络上URL,我利用ngrok可以将本地服务器暴露到Internet上。
安装ngrok之后,在命令行中,敲上这样子的代码
./ngrok http 3333
得到
提示:ngrok输出HTTP和HTTPS地址,它们都映射到本地服务。
现在,一旦你添加webhook,GitHub立即提交测试post请求来决定是否设置正确。
测试一个commit吧我们把一切都做好了,现在我们去提交一个commit,一旦你这么做了,一个消息推送就发送到我们的浏览器中了。 如下图
Host PWA一个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
项目PWA代码
项目API code代码
个人博客地址
如果有那个地方翻译出错或者失误,请各位大神不吝赐教,小弟感激不尽
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/92190.html
摘要:基本上是使用现代技术构建的网站但是体验上却像一个移动,在年,谷歌工程师和创造了。此后谷歌就一直致力于让能给用户像原生一般的体验。检查谷歌浏览器的和现在重载你的并且打开,到选项去查看面板,确保这个选项是勾选的。 Web开发多年来有了显著的发展。它允许开发人员部署网站或Web应用程序并在数分钟内为全球数百万人服务。只需一个浏览器,用户可以输入URL就可以访问Web应用程序了。随着 Prog...
摘要:基本上是使用现代技术构建的网站但是体验上却像一个移动,在年,谷歌工程师和创造了。此后谷歌就一直致力于让能给用户像原生一般的体验。检查谷歌浏览器的和现在重载你的并且打开,到选项去查看面板,确保这个选项是勾选的。 Web开发多年来有了显著的发展。它允许开发人员部署网站或Web应用程序并在数分钟内为全球数百万人服务。只需一个浏览器,用户可以输入URL就可以访问Web应用程序了。随着 Prog...
摘要:基本上是使用现代技术构建的网站但是体验上却像一个移动,在年,谷歌工程师和创造了。此后谷歌就一直致力于让能给用户像原生一般的体验。检查谷歌浏览器的和现在重载你的并且打开,到选项去查看面板,确保这个选项是勾选的。 Web开发多年来有了显著的发展。它允许开发人员部署网站或Web应用程序并在数分钟内为全球数百万人服务。只需一个浏览器,用户可以输入URL就可以访问Web应用程序了。随着 Prog...
摘要:在上一篇,介绍一下渐进式离线的文章中,我们讨论了典型的应该是什么样子的并且同时也介绍了。暴露了一个异步,以避免阻塞的加载。但一些研究表明,在某些情况下,它是阻塞的。打开并且添加如下代码清除缓存并重新加载。 在上一篇,介绍一下渐进式 Web App(离线) - Part 1的文章中,我们讨论了典型的pwa应该是什么样子的并且同时也介绍了 server worker。到目前为止,我们已经缓...
阅读 1273·2021-11-04 16:09
阅读 3336·2021-10-19 11:45
阅读 2362·2021-10-11 10:59
阅读 984·2021-09-23 11:21
阅读 2743·2021-09-22 10:54
阅读 1107·2019-08-30 15:53
阅读 2577·2019-08-30 15:53
阅读 3454·2019-08-30 12:57