摘要:本文是學習與實踐系列的第五篇文章。實際上,消息推送與提醒是兩個功能和。在這一篇里,我們先來學習如何使用進行消息推送。而當服務端要推送消息時,會使用私鑰對發送的數據進行數字簽名,并根據數字簽名生成一個叫請求頭。
《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。
本文是《PWA學習與實踐》系列的第五篇文章。文中的代碼都可以在learning-pwa的push分支上找到(git clone后注意切換到push分支)。
PWA作為時下最火熱的技術概念之一,對提升Web應用的安全、性能和體驗有著很大的意義,非常值得我們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。
1. 引言在之前的幾篇文章中,我和大家分享了如何使用manifest(以及meta標簽)讓你的Web App更加“native”;以及如何使用Service Worker來cache資源,加速Web App的訪問速度,提供部分離線功能。在接下來的內容里,我們會探究PWA中的另一個重要功能——消息推送與提醒(Push & Notification)。這個能力讓我們可以從服務端向用戶推送各類消息并引導用戶觸發相應交互。
實際上,消息推送與提醒是兩個功能——Push API 和 Notification API。為了大家能夠更好理解其中的相關技術,我也會分為Push(推送消息)與Notification(展示提醒)兩部分來介紹。在這一篇里,我們先來學習如何使用Push API進行消息推送。
Push API 和 Notification API其實是兩個獨立的技術,完全可以分開使用;不過Push API 和 Notification API相結合是一個常見的模式。2. 瀏覽器是如何實現服務器消息Push的
Web Push的整個流程相較之前的內容來說有些復雜。因此,在進入具體技術細節之前,我們需要先了解一下整個Push的基本流程與相關概念。
如果你對Push完全不了解,可能會認為,Push是我們的服務端直接與瀏覽器進行交互,使用長連接、WebSocket或是其他技術手段來向客戶端推送消息。然而,這里的Web Push并非如此,它其實是一個三方交互的過程。
在Push中登場的三個重要“角色”分別是:
瀏覽器:就是我們的客戶端
Push Service:專門的Push服務,你可以認為是一個第三方服務,目前chrome與firefox都有自己的Push Service Service。理論上只要瀏覽器支持,可以使用任意的Push Service
后端服務:這里就是指我們自己的后端服務
下面就介紹一下這三者在Web Push中是如何交互。
2.1. 消息推送流程下圖來自Web Push協議草案,是Web Push的整個流程:
+-------+ +--------------+ +-------------+ | UA | | Push Service | | Application | +-------+ +--------------+ | Server | | | +-------------+ | Subscribe | | |--------------------->| | | Monitor | | |<====================>| | | | | | Distribute Push Resource | |-------------------------------------------->| | | | : : : | | Push Message | | Push Message |<---------------------| |<---------------------| | | | |
該時序圖表明了Web Push的各個步驟,我們可以將其分為訂閱(subscribe)與推送(push)兩部分來看。
subscribe,首先是訂閱:
Ask Permission:這一步不再上圖的流程中,這其實是瀏覽器中的策略。瀏覽器會詢問用戶是否允許通知,只有在用戶允許后,才能進行后面的操作。
Subscribe:瀏覽器(客戶端)需要向Push Service發起訂閱(subscribe),訂閱后會得到一個PushSubscription對象
Monitor:訂閱操作會和Push Service進行通信,生成相應的訂閱信息,Push Service會維護相應信息,并基于此保持與客戶端的聯系;
Distribute Push Resource:瀏覽器訂閱完成后,會獲取訂閱的相關信息(存在于PushSubscription對象中),我們需要將這些信息發送到自己的服務端,在服務端進行保存。
Push Message,然后是推送:
Push Message階段一:我們的服務端需要推送消息時,不直接和客戶端交互,而是通過Web Push協議,將相關信息通知Push Service;
Push Message階段二:Push Service收到消息,通過校驗后,基于其維護的客戶端信息,將消息推送給訂閱了的客戶端;
最后,客戶端收到消息,完成整個推送過程。
2.2. 什么是Push Service在上面的Push流程中,出現了一個比較少接觸到的角色:Push Service。那么什么是Push Service呢?
A push service receives a network request, validates it and delivers a push message to the appropriate browser.
Push Service可以接收網絡請求,校驗該請求并將其推送給合適的瀏覽器客戶端。Push Service還有一個非常重要的功能:當用戶離線時,可以幫我們保存消息隊列,直到用戶聯網后再發送給他們。
目前,不同的瀏覽器廠商使用了不同的Push Service。例如,chrome使用了google自家的FCM(前身為GCM),firefox也是使用自家的服務。那么我們是否需要寫不同的代碼來兼容不同的瀏覽器所使用的服務呢?答案是并不用。Push Service遵循Web Push Protocol,其規定了請求及其處理的各種細節,這就保證了,不同的Push Service也會具有標準的調用方式。
這里再提一點:我們在上一節中說了Push的標準流程,其中第一步就是瀏覽器發起訂閱,生成一個PushSubscription對。Push Service會為每個發起訂閱的瀏覽器生成一個唯一的URL,這樣,我們在服務端推送消息時,向這個URL進行推送后,Push Service就會知道要通知哪個瀏覽器。而這個URL信息也在PushSubscription對象里,叫做endpoint。
那么,如果我們知道了endpoint的值,是否就代表我們可以向客戶端推送消息了呢?并非如此。下面會簡單介紹一下Web Push中的安全策略。
2.3. 如何保證Push的安全性在Web Push中,為了保證客戶端只會收到其訂閱的服務端推送的消息(其他的服務端即使在拿到endpoint也無法推送消息),需要對推送信息進行數字簽名。該過程大致如下:
在Web Push中會有一對公鑰與私鑰。客戶端持有公鑰,而服務端持有私鑰。客戶端在訂閱時,會將公鑰發送給Push Service,而Push Service會將該公鑰與相應的endpoint維護起來。而當服務端要推送消息時,會使用私鑰對發送的數據進行數字簽名,并根據數字簽名生成一個叫】Authorization請求頭。Push Service收到請求后,根據endpoint取到公鑰,對數字簽名解密驗證,如果信息相符則表明該請求是通過對應的私鑰加密而成,也表明該請求來自瀏覽器所訂閱的服務端。反之亦然。
而公鑰與私鑰如何生成,會在第三部分的實例中講解。
3. 如何使用Push API來推送向用戶推送信息到這里,我們已經基本了解了Web Push的流程。光說不練假把式,下面我就通過具體代碼來說明如何使用Web Push。
這部分會基于sw-cache分支上的代碼,繼續增強我們的“圖書搜索”WebApp。
為了使文章與代碼更清晰,將Web Push分為這幾個部分:
瀏覽器發起訂閱,并將訂閱信息發送至后端;
將訂閱信息保存在服務端,以便今后推送使用;
服務端推送消息,向Push Service發起請求;
瀏覽器接收Push信息并處理。
友情提醒:由于Chrome所依賴的Push Service——FCM在國內不可訪問,所以要正常運行demo中的代碼需要“梯子”,或者可以選擇Firefox來進行測試。3.1. 瀏覽器(客戶端)生成subscription信息
首先,我們需要使用PushManager的subscribe方法來在瀏覽器中進行訂閱。
在《讓你的WebApp離線可用》中我們已經知道了如何注冊Service Worker。當我們注冊完Service Worker后會得到一個Registration對象,通過調用Registration對象的registration.pushManager.subscribe()方法可以發起訂閱。
為了使代碼更清晰,本篇demo在之前的基礎上,先抽離出Service Worker的注冊方法:
// index.js function registerServiceWorker(file) { return navigator.serviceWorker.register(file); }
然后定義了subscribeUserToPush()方法來發起訂閱:
// index.js function subscribeUserToPush(registration, publicKey) { var subscribeOptions = { userVisibleOnly: true, applicationServerKey: window.urlBase64ToUint8Array(publicKey) }; return registration.pushManager.subscribe(subscribeOptions).then(function (pushSubscription) { console.log("Received PushSubscription: ", JSON.stringify(pushSubscription)); return pushSubscription; }); }
這里使用了registration.pushManager.subscribe()方法中的兩個配置參數:userVisibleOnly和applicationServerKey。
userVisibleOnly表明該推送是否需要顯性地展示給用戶,即推送時是否會有消息提醒。如果沒有消息提醒就表明是進行“靜默”推送。在Chrome中,必須要將其設置為true,否則瀏覽器就會在控制臺報錯:
applicationServerKey是一個客戶端的公鑰,VAPID定義了其規范,因此也可以稱為VAPID keys。如果你還記得2.3中提到的安全策略,應該對這個公鑰不陌生。該參數需要Unit8Array類型。因此定義了一個urlBase64ToUint8Array方法將base64的公鑰字符串轉為Unit8Array。subscribe()也是一個Promise方法,在then中我們可以得到訂閱的相關信息——一個PushSubscription對象。下圖展示了這個對象中的一些信息。注意其中的endpoint,Push Service會為每個客戶端隨機生成一個不同的值.
之后,我們再將PushSubscription信息發送到后端。這里定義了一個sendSubscriptionToServer()方法,該方法就是一個普通的XHR請求,會向接口post訂閱信息,為了節約篇幅就不列出具體代碼了。
最后,將這一系列方法組合在一起。當然,使用Web Push前,還是需要進行特性檢測"PushManager" in window。
// index.js if ("serviceWorker" in navigator && "PushManager" in window) { var publicKey = "BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A"; // 注冊service worker registerServiceWorker("./sw.js").then(function (registration) { console.log("Service Worker 注冊成功"); // 開啟該客戶端的消息推送訂閱功能 return subscribeUserToPush(registration, publicKey); }).then(function (subscription) { var body = {subscription: subscription}; // 為了方便之后的推送,為每個客戶端簡單生成一個標識 body.uniqueid = new Date().getTime(); console.log("uniqueid", body.uniqueid); // 將生成的客戶端訂閱信息存儲在自己的服務器上 return sendSubscriptionToServer(JSON.stringify(body)); }).then(function (res) { console.log(res); }).catch(function (err) { console.log(err); }); }
注意,這里為了方便我們后面的推送,為每個客戶端生成了一個唯一IDuniqueid,這里使用了時間戳生成簡單的uniqueid。
此外,由于userVisibleOnly為true,所以需要用戶授權開啟通知權限,因此我們會看到下面的提示框,選擇“允許”即可。你可以在設置中進行通知的管理。
3.2. 服務端存儲客戶端subscription信息為了存儲瀏覽器post來的訂閱信息,服務端需要增加一個接口/subscription,同時添加中間件koa-body用于處理body
// app.js const koaBody = require("koa-body"); /** * 提交subscription信息,并保存 */ router.post("/subscription", koaBody(), async ctx => { let body = ctx.request.body; await util.saveRecord(body); ctx.response.body = { status: 0 }; });
接收到subscription信息后,需要在服務端進行保存,你可使用任何方式來保存它:mysql、redis、mongodb……這里為了方便,我使用了nedb來進行簡單的存儲。nedb不需要部署安裝,可以將數據存儲在內存中,也可以持久化,nedb的api和mongodb也比較類似。
這里util.saveRecord()做了這些工作:首先,查詢subscription信息是否存在,若已存在則只更新uniqueid;否則,直接進行存儲。
至此,我們就將客戶端的訂閱信息存儲完畢了。現在,就可以等待今后推送時使用。
3.3. 使用subscription信息推送信息在實際中,我們一般會給運營或產品同學提供一個推送配置后臺。可以選擇相應的客戶端,填寫推送信息,并發起推送。為了簡單起見,我并沒有寫一個推送配置后臺,而只提供了一個post接口/push來提交推送信息。后期我們完全可以開發相應的推送后臺來調用該接口。
// app.js /** * 消息推送API,可以在管理后臺進行調用 * 本例子中,可以直接post一個請求來查看效果 */ router.post("/push", koaBody(), async ctx => { let {uniqueid, payload} = ctx.request.body; let list = uniqueid ? await util.find({uniqueid}) : await util.findAll(); let status = list.length > 0 ? 0 : -1; for (let i = 0; i < list.length; i++) { let subscription = list[i].subscription; pushMessage(subscription, JSON.stringify(payload)); } ctx.response.body = { status }; });
來看一下/push接口。
首先,根據post的參數不同,我們可以通過uniqueid來查詢某條訂閱信息:util.find({uniqueid});也可以從數據庫中查詢出所有訂閱信息:util.findAll()。
然后通過pushMessage()方法向Push Service發送請求。根據第二節的介紹,我們知道,該請求需要符合Web Push協議。然而,Web Push協議的請求封裝、加密處理相關操作非常繁瑣。因此,Web Push為各種語言的開發者提供了一系列對應的庫:Web Push Libaray,目前有NodeJS、PHP、Python、Java等。把這些復雜而繁瑣的操作交給它們可以讓我們事半功倍。
最后返回結果,這里只是簡單的根據是否有訂閱信息來進行返回。
安裝node版web-push
npm install web-push --save
前面我們提到的公鑰與私鑰,也可以通過web-push來生成
使用web-push非常簡單,首先設置VAPID keys:
// app.js const webpush = require("web-push"); /** * VAPID值 * 這里可以替換為你業務中實際的值 */ const vapidKeys = { publicKey: "BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A", privateKey: "TVe_nJlciDOn130gFyFYP8UiGxxWd3QdH6C5axXpSgM" }; // 設置web-push的VAPID值 webpush.setVapidDetails( "mailto:alienzhou16@163.com", vapidKeys.publicKey, vapidKeys.privateKey );
設置完成后即可使用webpush.sendNotification()方法向Push Service發起請求。
最后我們來看下pushMessage()方法的細節:
// app.js /** * 向push service推送信息 * @param {*} subscription * @param {*} data */ function pushMessage(subscription, data = {}) { webpush.sendNotification(subscription, data, options).then(data => { console.log("push service的相應數據:", JSON.stringify(data)); return; }).catch(err => { // 判斷狀態碼,440和410表示失效 if (err.statusCode === 410 || err.statusCode === 404) { return util.remove(subscription); } else { console.log(subscription); console.log(err); } }) }
webpush.sendNotification為我們封裝了請求的處理細節。狀態碼401和404表示該subscription已經無效,可以從數據庫中刪除。
3.4. Service Worker監聽Push消息調用webpush.sendNotification()后,我們就已經把消息發送至Push Service了;而Push Service會將我們的消息推送至瀏覽器。
要想在瀏覽器中獲取推送信息,只需在Service Worker中監聽push的事件即可:
// sw.js self.addEventListener("push", function (e) { var data = e.data; if (e.data) { data = data.json(); console.log("push的數據為:", data); self.registration.showNotification(data.text); } else { console.log("push沒有任何數據"); } });4. 效果展示
我們同時使用firefox與chrome來訪問該WebApp,并分別向這兩個客戶端推送消息。我們可以使用console中打印出來的uniqueid,在postman中發起/push請求進行測試。
可以看到,我們分別向firefox與chrome中推送了“welcome to PWA”這條消息。console中的輸出來自于Service Worker中對push事件的監聽。而彈出的瀏覽器提醒則來自于之前提到的、訂閱時配置的userVisibleOnly: true屬性。在后續的文章里,我繼續帶大家了解Notification API(提醒)的使用。
正如前文所述,Push Service可以在設備離線時,幫你維護推送消息。當瀏覽器設備重新聯網時,就會收到該推送。下面展示了在設備恢復聯網后,就會收到推送:
5. 萬惡的兼容性又到了查看兼容性的時間了。比較重要的是,對于Push API,目前Safari團隊并沒有明確表態計劃支持。
當然,其實比兼容性更大的一個問題是,Chrome所依賴的FCM服務在國內是無法訪問的,而Firefox的服務在國內可以正常使用。這也是為什么在代碼中會有這一項設置:
const options = { // proxy: "http://localhost:1087" // 使用FCM(Chrome)需要配置代理 };
上面代碼其實是用來配置web-push代理的。這里有一點需要注意,目前從npm上安裝的web-push是不支持設置代理選項的。針對這點github上專門有issue進行了討論,并在最近(兩周前)合入了相應的PR。因此,如果需要web-push支持代理,簡單的方式就是基于master進行web-push代碼的相應調整。
雖然由于google服務被屏蔽,導致國內Push功能無法在chrome上使用,但是作為一個重要的技術點,Web Push還是非常值得我們了解與學習的。
6. 寫在最后本文中所有的代碼示例均可以在learn-pwa/push上找到。注意在git clone之后,切換到push分支。切換其他分支可以看到不同的版本:
basic分支:基礎項目demo,一個普通的圖書搜索應用(網站);
manifest分支:基于basic分支,添加manifest等功能;
sw-cache分支:基于manifest分支,添加緩存與離線功能;
push分支:基于sw-cache分支,添加服務端消息推送功能;
master分支:應用的最新代碼。
如果你喜歡或想要了解更多的PWA相關知識,歡迎關注我,關注《PWA學習與實踐》系列文章。我會總結整理自己學習PWA過程的遇到的疑問與技術點,并通過實際代碼和大家一起實踐。
在下一篇文章里,我們先緩下腳步——工欲善其事,必先利其器。在繼續了解更多PWA相關技術之前,先了解一些chrome上的PWA調試技巧。之后,我們會再回來繼續了解另一個經常與Push API組合在一起的功能——消息提醒,Notification API。
《PWA學習與實踐》系列第一篇:2018,開始你的PWA學習之旅
第二篇:10分鐘學會使用Manifest,讓你的WebApp更“Native”
第三篇:從今天起,讓你的WebApp離線可用
第四篇:TroubleShooting: 解決FireBase login驗證失敗問題
第五篇:與你的用戶保持聯系: Web Push功能(本文)
第六篇:How to Debug? 在chrome中調試你的PWA
第七篇:增強交互:使用Notification API來進行提醒
第八篇:使用Service Worker進行后臺數據同步
第九篇:PWA實踐中的問題與解決方案
第十篇:Resource Hint - 提升頁面加載性能與體驗
第十一篇:從PWA離線工具集workbox中學習各類離線策略(寫作中…)
參考資料Generic Event Delivery Using HTTP Pus (draft-ietf-webpush-protocol-12)
FCM簡單介紹
How Push Works
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99011.html
摘要:簡稱,是提升的體驗的一種新方法,能給用戶原生應用的體驗。當網站以這種方式啟動時它具有唯一的圖標和名稱,以便用戶將其與其他網站區分開來。表示啟動時的方向,橫屏豎屏等,具體參數值可參考文檔。下一篇文章中,主要講述在實踐中的重要能力。 這周,Chrome 70正式版本發布,Progressive Web Apps(PWA)已經正式支持到Windows 10平臺,然而,早在前幾個版本之前,Ch...
摘要:學習與實踐系列文章已整理至學習手冊,文字內容已同步至。本文是學習與實踐系列的第三篇文章。引言其中一個令人著迷的能力就是離線可用。但是,如果你注意到文章開頭的圖片就會發現,離線時我們不僅可以訪問,還可以使用搜索功能。 《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。 本文是《PWA學習與實...
摘要:如果返回的被拒,另一個同步事件被自動地開始重試操作,直到返回一個成功狀態的。推送機制使得服務器能夠向發送信息,然后將信息展示給用戶才是消息通知。然后它們可以發送消息通知,或者是更新的狀態。 原文地址:https://medium.freecodecamp.org/service-workers-the-little-heroes-behind-progressive-web-apps-...
摘要:安裝事件綁定在文件中,當安裝成功后,事件就會被觸發。激活當安裝完成后并進入激活狀態,會觸發事件。這會導致更新得不到響應。由兩個構成用來顯示系統的通知用來處理下發的消息這兩個都是建立在在基礎上的,在后臺響應推送消息時間,并把他們傳遞給應用。 showImg(https://segmentfault.com/img/bVbhbQf?w=1182&h=656); 原文地址: https://...
閱讀 2621·2021-11-25 09:43
閱讀 2724·2021-11-04 16:09
閱讀 1634·2021-10-12 10:13
閱讀 881·2021-09-29 09:35
閱讀 880·2021-08-03 14:03
閱讀 1777·2019-08-30 15:55
閱讀 2989·2019-08-28 18:14
閱讀 3489·2019-08-26 13:43