在第一篇:介紹一下漸進式 Web App(離線) - Part 1中咱們介紹了一個典型的PWA應該是什麼樣子的,而且介紹了一下sercer worker和應用殼(app shell),在第二篇介紹一下漸進式 Web App(即時加載) - Part 2 文章中,咱們緩存了動態數據,並實現了從本地保存的數據中即時加載數據到頁面中,也順便介紹了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 代碼以下web
(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);
複製代碼
上面的代碼作了不少事情。放心啦,我將會解釋一波代碼的功能滴。mongodb
//Push notification button
var fabPushElement = document.querySelector('.fab__push');
var fabPushImgElement = document.querySelector('.fab__image');
複製代碼
上面的代碼獲取推送通知激活和停用按鈕的節點。shell
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';
}
}
複製代碼
用戶訂閱按鈕的樣式改變json
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。
添加下面的代碼到你的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線上的地址:ril-pwa.firebaseapp.com/ 服務器api線上地址:rilapi.herokuapp.com/api
打開你的設備上的瀏覽器,尤爲是Chrome,並像這樣添加它:
看到桌面上已經有了 應用圖標了。而後本教程也是總結鳥。
~~~~~ 完結,散花散花散花散花 ~~~~
附:
點擊連接查看
第一篇: 介紹一下漸進式 Web App(離線) - Part 1
第二篇: 介紹一下漸進式 Web App(即時加載)- Part 2
若是有那個地方翻譯出錯或者失誤,請各位大神不吝賜教,小弟感激涕零