騰訊雲技術社區-掘金主頁持續爲你們呈現雲計算技術文章,歡迎你們關注!javascript
做者:villainthrhtml
摘自 前端小吉米前端
伴隨着今年 Google I/O 大會的召開,一個很火的概念--Progressive Web Apps 誕生了。這表明着咱們 web 端有了和原生 APP 媲美的能力。可是,有一個很重要的痛點,web 一直不能使用消息推送,雖然,後面提出了 Notification
API,但這須要網頁持續打開,這對於常規 APP 實現的推送,根本就不是一個量級的。因此,開發者一直在呼籲能不能退出一款可以在網頁關閉狀況下的 web 推送呢?
如今,Web 時代已經到來!
爲了作到在網頁關閉的狀況下,還能繼續發送 Notification,咱們就只能使用駐留進程。而如今 Web 的駐留進程就是如今正在大力普及的 Service Worker。換句話說,咱們的想要實現斷線 Notification 的話,須要用的技術棧是:java
這裏,我先一個簡單的 demo 樣式。node
說實在的,我其實 TM 很煩的這 Noti。通常使用 PC 端的,也沒見有啥消息彈出來,可是,如今好了 Web 一搞,結果三端通用。你若是不由用的話,保不許每天彈。。。git
SW(Service Worker) 我已經在前一篇文章裏面講清楚了。這裏主要探究一下另外兩個技術 Push
和 Notification
。首先,有一個問題,這兩個技術是用來幹嗎的呢?github
這兩個技術,咱們能夠理解爲就是 server 和 SW 之間,SW 和 user 之間的消息通訊。web
能夠看出,兩個技術是緊密鏈接到一塊兒的。這裏,咱們先來說解一下 notification
的相關技術。算法
那如今,咱們想給用戶發送一個消息的話應該怎麼發送呢?
代碼很簡單,我直接放了:chrome
self.addEventListener('push', function(event) {
var title = 'Yay a message.';
var body = 'We have received a push message.';
var icon = '/images/icon-192x192.png';
var tag = 'simple-push-demo-notification-tag';
var data = {
doge: {
wow: 'such amaze notification data'
}
};
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: data
})
);
});複製代碼
你們一開始看見這個代碼,可能會以爲有點陌生。實際上,這裏是結合 SW 來完成的。push
是 SW 接收到後臺的 push 信息而後出發。固然,咱們獲取信息的主要途徑也是從 event
中獲取的。這裏爲了簡便,就直接使用寫死的信息了。大體解釋一下 API。
不過,咱們須要記住的是 SW 中的 notification 只是很早之前就退出的桌面 notification 的繼承對象。這意味着,你們若是想要嘗試一下 notification,並不須要手動創建一個 notification,而只要使用
// 桌面端
var not = new Notification("show note", { icon: "newsong.svg", tag: "song" });
not.onclick = function() { dosth(this); };
// 在 SW 中使用
self.registration.showNotification("New mail from Alice", {
actions: [{action: 'archive', title: "Archive"}]
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'archive') {
silentlyArchiveEmail();
} else {
clients.openWindow("/inbox");
}
}, false);複製代碼
不過,若是你想設置本身想要的 note 效果的話,則須要瞭解一下,showNotification 裏面具體每次參數表明的含義,參考 Mozilla,咱們能夠了解到基本的使用方式。如上,API 的基本格式爲 showNotification(title, [options])
notificationclick
來進行相關處理actions 的上限值,一般根據 Notification.maxActions
肯定。經過在 Not 中定義好 actions 觸發,最後咱們會經過,監聽的 notificationclick
來作相關處理:
self.addEventListener('notificationclick', function(event) {
var messageId = event.notification.data;
event.notification.close();
// 經過設置的 actions 來作適當的響應
if (event.action === 'like') {
silentlyLikeItem();
}
else if (event.action === 'reply') {
clients.openWindow("/messages?reply=" + messageId);
}
else {
clients.openWindow("/messages?reply=" + messageId);
}
}, false);複製代碼
/audio/notification-sound.mp3
notificationclick
事件中,對回調參數進行調用event.notification.data
。針對於推送的圖片來講,可能會針對不一樣的手機用到的圖片尺寸會有所區別,例如,針對不一樣的 dpi。
具體參照:
看下 MDN 提供的 demo:
function showNotification() {
Notification.requestPermission(function(result) {
if (result === 'granted') {
navigator.serviceWorker.ready.then(function(registration) {
registration.showNotification('Vibration Sample', {
body: 'Buzz! Buzz!',
icon: '../images/touch/chrome-touch-icon-192x192.png',
vibrate: [200, 100, 200, 100, 200, 100, 200],
tag: 'vibration-sample'
});
});
}
});
}複製代碼
固然,簡單 API 的使用就是上面那樣。可是,若是咱們不加剋制的使用 Not,可能會讓用戶徹底屏蔽掉咱們的推送,得不償失。因此,咱們須要遵循必定的原則去發送。
遵循時間,地點,人物要素進行相關信息的設置。
雖然這看起來有點違背咱們最初的意圖。不過,這樣確實可以提升用戶的體驗。好比在信息回覆中,直接顯示:XX回覆:...
這樣的格式,能夠徹底省去用戶的打開網頁的麻煩。
好比:
correct:
由於頗有可能形成推送信息重複
由於,Not 已經幫你寫好了
沒用的 icon:
實際上,Not 並不全在 SW 中運行,對於設計用戶初始權限,咱們須要在主頁面中,作出相關的響應。固然,在設置推送的時候,咱們須要考慮到用戶是否會禁用,這裏影響仍是特別大的。
咱們,獲取用戶權限通常能夠直接使用 Notification
上掛載的 permission
屬性來獲取的。
簡單的來講爲:
function initialiseState() {
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
return;
}
// 檢查是否能夠進行服務器推
if (!('PushManager' in window)) {
return;
}
// 是否被禁用
if (Notification.permission === 'denied') {
return;
}
if (Notification.permission === 'granted') {
// dosth();
return;
}
// 若是還處於默認狀況下,則進行詢問
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// 檢查訂閱
serviceWorkerRegistration.pushManager.getSubscription()
.then(function(subscription) {
// 檢查是否已經被訂閱
if (!subscription) {
// 沒有
return;
}
// 有
// doSth();
})
.catch(function(err) {
window.Demo.debug.log('Error during getSubscription()', err);
});
});
}複製代碼
咱們在加載的時候,須要先進行檢查一遍,若是是默認狀況,則須要發起訂閱的請求。而後再開始進行處理。
那,咱們上面的那段代碼該放在哪一個位置呢?首先,這裏使用到了 SW,這意味着,咱們須要將 SW 先註冊成功才行。實際代碼應放在 SW 註冊成功的回調中:
window.addEventListener('load', function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(initialiseState);
} else {
window.Demo.debug.log('Service workers aren\'t supported in this browser.');
}
});複製代碼
爲了更好的顯示信息,咱們還能夠將受權代碼放到後面去。好比,將 subscribe 和 btn 的 click 事件進行綁定。這時候,咱們並不須要考慮 SW 是否已經註冊好了,由於SW 的註冊時間遠遠不及用戶的反應時間。
例如:
var pushButton = document.querySelector('.js-push-button');
pushButton.addEventListener('click', function() {
if (isPushEnabled) {
unsubscribe();
} else {
subscribe();
}
});複製代碼
咱們具體看一下 subscribe 內容:
function subscribe() {
var pushButton = document.querySelector('.js-push-button');
pushButton.disabled = true;
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// 請求訂閱
serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
.then(function(subscription) {
isPushEnabled = true;
pushButton.textContent = 'Disable Push Messages';
pushButton.disabled = false;
return sendSubscriptionToServer(subscription);
})
});
}複製代碼
說道這裏,你們可能會看的雲裏霧裏,這裏咱們來具體看一下 serviceWorkerRegistration.pushManager
具體含義。該參數是從 SW 註冊事件回調函數獲取的。也就是說,它是咱們和 SW 交互的通道。該對象上,綁定了幾個獲取訂閱相關的 API:
例如:
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: new Uint8Array([...])
});複製代碼
爲了更好的體驗,咱們能夠將二者結合起來,進行相關推送檢查,具體的 load 中,則爲:
window.addEventListener('load', function() {
var pushButton = document.querySelector('.js-push-button');
pushButton.addEventListener('click', function() {
if (isPushEnabled) {
unsubscribe();
} else {
subscribe();
}
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(initialiseState);
} else {
window.Demo.debug.log('Service workers aren\'t supported in this browser.');
}
});複製代碼
固然,這裏面還會涉及其餘的一些細節,我這裏就不過多贅述了。詳情能夠查閱: Notification demo。
咱們開啓一個 Not 詢問很簡單,但關鍵是,若是讓用戶贊成。若是咱們一開始就進行詢問,這樣成功性的可能性過低。咱們能夠在頁面加載後進行詢問。這裏,也有一些提醒原則:
經過具體行爲進行詢問
好比,當我在查詢車票時,就可讓用戶在退出時選擇是否接受推送信息。好比,國外的飛機延遲通知網頁:
讓用戶來決定是否進行推送
由於用戶不是技術人員,咱們須要將一些接口,暴露給用戶。針對推送而言,咱們可讓用戶選擇是否進行推送,而且,在提示的同時,顯示的信息應該儘可能和用戶相關。
web push 在實際協議中,會設計到兩個 server,比較複雜,這裏咱們先來看一下。client 是如何處理接受到的信息的。
當 SW 接受到 server 傳遞過來的信息時,會先觸發 push
事件。咱們一般作以下處理:
self.addEventListener('push', function(event) {
if (event.data) {
console.log('This push event has data: ',event.data.text());
} else {
console.log('This push event has no data.');
}
});複製代碼
其中,咱們經過 server push 過來的 msg 一般是掛載到 event.data
裏的。而且,該部署了 Response 的相關 API:
咱們知道 Service Worker 並非常駐進程,有童鞋可能會問到,那怎麼利用 SW 監聽 push 事件呢?
這裏就不用擔憂了,由於瀏覽器本身會打開一個端口監聽接受到的信息,而後喚起指定的 SW(若是你的瀏覽器是關閉的,那麼你能夠洗洗睡了)。並且,因爲這樣隨機關閉的機制,咱們須要上述提到的 event.waitUntil
API 來幫助咱們完成持續 alive SW 的效果,防止正在執行的異步程序被終止。針對於咱們的 notification 來講,實際上就是一個異步,因此,咱們須要使用上述 API 進行包裹。
self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
event.waitUntil(promiseChain);
});複製代碼
固然,若是你想在 SW 裏面作更多的異步事情的話,可使用 Promise.all 進行包裹。
self.addEventListener('push', function(event) {
const promiseChain = Promise.all([ async1,async2 ]);
event.waitUntil(promiseChain);
});複製代碼
以後,就是將具體信息展現推送給用戶了。上面已經將了具體 showNotification
裏面的參數有哪些。不過,這可能不夠直觀,咱們可使用一張圖來感覺一下:
(左:firefox,右:Chrome)
另外,在 showNotification options 裏面,還有一些屬性須要咱們額外注意。
對於指定的 Not 咱們可使用 tag
來代表其惟一性,這表明着當咱們在使用相同 tag
的 Not 時,上一條 Not 會被最新擁有同一個 tag 的Not 替換。即:
const title = 'First Notification';
const options = {
body: 'With \'tag\' of \'message-group-1\'',
tag: 'message-group-1'
};
registration.showNotification(title, options);複製代碼
顯示樣式爲:
接着,我顯示一個不一樣 tag 的 Not:
const title = 'Second Notification';
const options = {
body: 'With \'tag\' of \'message-group-2\'',
tag: 'message-group-2'
};
registration.showNotification(title, options);複製代碼
結果爲:
而後,我使用一個一樣 tag 的 Not:
const title = 'Third Notification';
const options = {
body: 'With \'tag\' of \'message-group-1\'',
tag: 'message-group-1'
};
registration.showNotification(title, options);複製代碼
則相同的 tag 會被最新 tag 的 Not 替換:
該屬性是 Not 裏面又一個比較尷尬的屬性,它的實際應用場景是當有重複 Not 被替換時,震動和聲音能不能被重複播放,但默認爲 false。
那何爲重複呢?
就是,上面咱們提到的 tag 被替換。通常應用場景就是和同一個對象聊天時,發送多個信息來時,咱們不可能推送多個提示信息,通常就是把已經存在的 Not 進行替換就 ok,那麼這就是上面提到的由於重複,被替換的 Not。
通常咱們對於這樣的 Not 能夠設置爲:
const title = 'Second Notification';
const options = {
body: 'With "renotify: true" and "tag: \'renotify\'".',
tag: 'renotify',
renotify: true
};
registration.showNotification(title, options);複製代碼
而且,若是你設置了 renotify
而沒有設置 tag 的話,這是會報錯的 !!!
防止本身推送的 Not 發出任何額外的提示操做(震動,聲音)。默認爲 false。不過,咱們能夠在須要的時候,設置爲 true:
const title = 'Silent Notification';
const options = {
body: 'With "silent: \'true\'".',
silent: true
};
registration.showNotification(title, options);複製代碼
對於通常的 Not 來講,當展現必定時間事後,就能夠自行消失。不過,若是你的 Not 必定須要用戶去消除的話,可使用 requireInteraction
來進行長時間留存。通常它的默認值爲 false。
const title = 'Require Interaction Notification';
const options = {
body: 'With "requireInteraction: \'true\'".',
requireInteraction: true
};
registration.showNotification(title, options);複製代碼
如今,你的 Not 已經顯示給用戶,不過,默認狀況下,Not 自己是不會作任何處理的。咱們須要監聽用戶,對其的相關操做(其實就是 click 事件)。
self.addEventListener('notificationclick', function(event) {
// do nothing
});複製代碼
另外,經過咱們在 showNotification 裏面設置的 action
,咱們能夠根據其做出不一樣的響應。
self.addEventListener('notificationclick', function(event) {
if (event.action) {
console.log('Action Button Click.', event.action);
} else {
console.log('Notification Click.');
}
});複製代碼
這是應該算是最經常使用的一個,只是用來提示用戶的相關信息:
self.addEventListener('notificationclick', function(event) {
event.notification.close();
// Do something as the result of the notification click
});複製代碼
這裏,須要使用到咱們的 service 裏面的一個新的 API clients。
event.waitUntil(
// examplePage 就是當前頁面的 url
clients.openWindow(examplePage)
);複製代碼
這裏須要注意的是 examplePage
必須是和當前 SW 同域名才行。不過,這裏有兩種狀況,須要咱們考慮:
這裏,咱們能夠利用 cilents 提供的相關 API 獲取,當前瀏覽器已經打開的頁面 URLs。不過這些 URLs 只能是和你 SW 同域的。而後,經過匹配 URL,經過 matchingClient.focus()
進行聚焦。沒有的話,則新打開頁面便可。
const urlToOpen = self.location.origin + examplePage;
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);複製代碼
另外,若是用戶已經停留在當前的網頁,那咱們可能就不須要推送了,那麼針對於這種狀況,咱們應該怎麼檢測用戶是否正在網頁上呢?
const promiseChain = ({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
let mustShowNotification = true;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
mustShowNotification = false;
break;
}
}
return mustShowNotification;
})
.then((mustShowNotification) => {
if (mustShowNotification) {
return self.registration.showNotification('Had to show a notification.');
} else {
console.log('Don\'t need to show a notification.');
}
});
event.waitUntil(promiseChain);複製代碼
固然,若是你本身的網頁已經被用戶打開,咱們一樣也能夠根據推送信息直接將信息傳遞給對應的 window。咱們經過 clients.matchAll
得到的 windowClient
對象,調用 postMessage
來進行消息的推送。
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString()
});複製代碼
該場景的主要針對消息的合併。好比,聊天消息,當有一個用戶給你發送一個消息時,你能夠直接推送,那若是該用戶又發送一個消息呢?
這時候,比較好的用戶體驗是直接將推送合併爲一個,而後替換便可。
那麼,此時咱們就須要得到當前已經展現的推送消息,這裏主要經過 registration.getNotifications()
API 來進行獲取。該 API 返回的也是一個 Promise 對象。
固然,咱們怎麼肯定兩個消息是同一我的發送的呢?這裏,就須要使用到,上面提到的 Not.data 的屬性。這是咱們在 showNotification
裏面附帶的,能夠直接在 Notification 對象中獲取。
return registration.getNotifications()
.then(notifications => {
let currentNotification;
for(let i = 0; i < notifications.length; i++) {
// 檢測已經存在的 Not.data.userName 和新消息的 userName 是否一致
if (notifications[i].data &&
notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
})
// 而後,進行相關的邏輯處理,將 body 的內容進行更替
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// We have an open notification, let's so something with it.
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});複製代碼
至關於從:
變爲:
上面提到了在 SW 中使用,clients 獲取窗口信息,這裏咱們先補充一下相關的知識。
咱們能夠將 Clients 理解爲咱們如今所在的瀏覽器,不過特殊的地方在於,它是遵照同域規則的,即,你只能操做和你域名一致的窗口。一樣,Clients 也只是一個集合,用來管理你當前全部打開的頁面,實際上,每一個打開的頁面都是使用一個 cilent object 進行表示的。這裏,咱們先來探討一下 cilent object:
type=window
的client。focused
沒太大的區別。可取值爲: hidden, visible, prerender, or unloaded
。而後,Clients Object 就是用來管理每一個窗口的。經常使用方法有:
self.clients.get(id).then(function(client) {
// 打開具體某個窗口
self.clients.openWindow(client.url);
});複製代碼
false
。只返回當前 SW 控制的窗口。all
。// 經常使用屬性爲:
clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then(function(clientList) {
for(var i = 0 ; i < clients.length ; i++) {
if(clientList[i].url === 'index.html') {
clients.openWindow(clientList[i]);
}
}
});複製代碼
先貼一張 google 關於 web push 的詳解圖:
上述圖,簡單闡述了從 server 產生信息,最終到手機生成提示信息的一系列過程。
先說一下中間那個 Message Server。這是獨立於咱們經常使用的 Server -> Client 的架構,瀏覽器能夠本身選擇 push service,開發者通常也不用關心。不過,若是你想使用本身定製的 push serivce 的話,只須要保證你的 service 可以提供同樣的 API 便可。上述過程爲:
applicationServerKey
。而後,phone 開始初始化 SW這裏,咱們能夠預先看一下 message server 返回來的內容:
{
"endpoint": "https://random-push-service.com/some-kind-of-unique-id-1234/v2/",
"keys": {
"p256dh" : "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=",
"auth" : "tBHItJI5svbpez7KI4CCXg=="
}
}複製代碼
endpoint
就是瀏覽器訂閱的 message server 的地址。這裏的 keys 咱們放到後面講解,主要就是用來進行 push message 的加密。
根據官方解釋,Message Server 與用戶將的通訊,借用的是 HTTP/2 的 server push 協議。上面的圖,其實能夠表達爲:
+-------+ +--------------+ +-------------+
| UA | | Push Service | | Application |
+-------+ +--------------+ | Server |
| | +-------------+
| Subscribe | |
|--------------------->| |
| Monitor | |
|<====================>| | | | | | Distribute Push Resource | |-------------------------------------------->| | | | : : : | | Push Message | | Push Message |<---------------------| |<---------------------| | | | |複製代碼
接下來,咱們就須要簡單的來看一下使用 Web Push 的基本原則。
那麼若是咱們想讓用戶訂閱咱們的 push service 咱們首先須要獲得用戶是否進行提示的許可。固然,一開始咱們還須要判斷一下,該用戶是否已經受權,仍是拒絕,或者是還未處理。這裏,能夠參考上面提到的推送權限一節中的 initialiseState
函數方法。
這裏咱們主要研究一下具體的訂閱環節(假設用戶已經贊成推送)。基本格式爲:
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
)
};
return registration.pushManager.subscribe(subscribeOptions);
)}
.then(function(subscription) {
return subscription
})複製代碼
這裏有兩個參數 userVisibleOnly
和 applicationServerKey
。這兩個屬性值具體表明什麼意思呢?
該屬性能夠算是強制屬性(你必須填,並且只能填 true)。由於,一開始 Notification 的設計是 能夠在用戶拒絕的狀況下繼續在後臺執行推送操做,這形成了另一種狀況,開發者能夠在用戶關閉的狀況下,經過 web push 獲取用戶的相關信息。因此,爲了安全性保證,咱們通常只能使用該屬性,而且只能爲 true(若是,不呢?瀏覽器就會報錯)。
前面說過它是一個 public key。用來加密 server 端 push 的信息。該 key 是一個 Uint8Array 對象,並且它 須要符合 VAPID 規範實際,因此咱們通常能夠叫作 application server keys
或者 VAPID keys
,咱們的 server 其實有私鑰和公鑰兩把鑰匙,這裏和 TLS/SSL 協商機制相似,不過不會協商出 session key,直接經過 pub/pri key 進行信息加/解密。不過,它還有其餘的用處:
整個流程圖爲:
另外,該 key 還有一個更重要的用途是,當在後臺 server 須要進行 push message,向 push service 發送請求時,會有一個 Authorization
頭,該頭的內容時由 private key 進行加密的。而後,push service 接受到以後,會根據配套的 endpoint 的 public key 進行解密,若是解密成功則表示該條信息是有效信息(發送的 server 是合法的)。
流程圖爲:
經過 subscribe() 異步調用返回的值 subscription
的具體格式爲:
{
"endpoint": "https://some.pushservice.com/something-unique",
"keys": {
"p256dh": "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
"auth":"FPssNDTKnInHVndSTdbKFw=="
}
}複製代碼
簡單說一下參數,endpoint 就是 push service 的 URL,咱們的 server 若是有消息須要推送,就是想該路由發送請求。而 keys 就是用來對信息加密的鑰匙。獲得返回的 subscription
以後,咱們須要發送給後臺 server 進行存儲。由於,每一個用戶的訂閱都會產生獨一無二的 endpoint,因此,咱們只須要將 endpoint 和關聯用戶存儲起來就 ok 了。
fetch('/api/save-subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})複製代碼
接下來就到了 server 推送 msg 的環節了。
當服務器有新的消息須要推送時,就須要向 push service 發送相關的請求進行 web push。不過,這裏咱們須要瞭解,從服務器到 push service
的請求,實際上就是 HTTP 的 post method。咱們看一個具體的請求例子:
POST /push-service/send/dbDqU8xX10w:APA91b... HTTP/1.1
Host: push.example.net
Push-Receipt: https://push.example.net/r/3ZtI4YVNBnUUZhuoChl6omU
TTL: 43200
Content-Type: text/plain;charset=utf8
Content-Length: 36
Authorization: WebPush
eyJ0eXAiOiJKV1QiLCJErtm.ysazNjjvW2L9OkSSHzvoD1oA
Crypto-Key:
p256ecdsa=BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU\_RCPCfA5aq9ojSwk5Y2EmClBPsiChYuI3jMzt3ir20P8r\_jgRR-dSuN182x7iB複製代碼
固然,變化的是裏面推送的具體的 Headers 和 body 內容。咱們能夠看一下具體頭部表明的意思:
Header | Content | |||
---|---|---|---|---|
Authorization | 能夠理解該頭是一個 JSON Web Token,用來驗證是不是真實的訂閱 server | |||
Crypto-Key | 用來表示加密的 key。它由兩部分組成:dh=publicKey,p256ecdsa=applicationServerKey 。其中 p256ecdsa 就是由 pub key 加密的 base64 的 url |
|||
Encryption | 它用來放置加鹽祕鑰。用來加密 payload | |||
Content-Type | 若是你沒發送 payload 的話,那麼就不用發送該頭。若是發送了,則須要將其設置爲 application/octet-stream 。這是爲了告訴瀏覽器我發送的是 stream data |
|||
Content-Length | 用來描述 payload 的長度(沒有 payload 的不用) | |||
Content-Encoding | 該頭必須一直是 aesgcm 不論你是否發送 payload |
|||
TTL (Time to Live) | 表示該 message 能夠在 push service 上停留多長時間(爲何停留?由於用戶沒有打開指定瀏覽器,push service 發佈過去)。若是 TTL 爲 0,表示當有推送信息時,而且此時 push service 可以和用戶的瀏覽器創建聯繫,則第一時間發送過去。不然當即失效 | |||
Topic | 該頭實際上和 Notification 中的 tag 頭相似。若是 server 前後發送了兩次擁有相同 Topic 的 message 請求,若是前一條 topic 正在 pending 狀態,則會被最新一條 topic 代替。不過,該 Topic 必須 <= 32 個字符 |
|||
Urgency[實驗特性] | 表示該消息的優先級,優先級高的 Notification 會優先發送。默認值爲: default 。可取值爲: "very-low" |
"low" | "normal" | "high" |
一般,push service 接受以後,會返回相關的狀態碼,來表示具體操做結果:
statusCode | Description |
---|---|
201 | 表示推送消息在 push service 中已經成功建立 |
429 | 此時,push service 有太多的推送請求,沒法響應你的請求。而且,push service 會返回 Retry-After 的頭部,表示你下次重試的時間。 |
400 | 無效請求,表示你的請求中,有不符合規範的頭部 |
413 | 你的 payload 過大。最小的 payload 大小爲 4kb |
能夠從上面頭部看出,push service 須要的頭很複雜,若是咱們純原生手寫的話,估計很快就寫煩了。這裏推薦一下 github 裏面的庫,能夠直接根據 app server key 來生成咱們想要的請求頭。這裏,咱們打算細節的瞭解一下每一個頭部內容產生的相關協議。
首先,這個 key 是怎麼拿到的?須要申請嗎?
答案是:不須要。這個 key 只要你符合必定規範就 ok。不過一旦生成以後,不要輕易改動,由於後面你會一直用到它進行信息交流。規則簡單來講爲:
crypto
加密庫,依照 P-256
曲線,生成`ECDSA` 簽名方式。簡單 demo 爲:
function generateVAPIDKeys() {
var curve = crypto.createECDH('prime256v1');
curve.generateKeys();
return {
publicKey: curve.getPublicKey(),
privateKey: curve.getPrivateKey(),
};
}
// 也能夠直接根據 web-push 庫生成
const vapidKeys = webpush.generateVAPIDKeys();複製代碼
具體頭部詳細信息以下:
Authorization 頭部的值(上面也提到了)是一個 JSON web token(簡稱爲 JWT)。基本格式爲:
Authorization: WebPush <JWT Info>.<JWT Payload>.<Signature>複製代碼
實際上,該頭涵蓋了不少信息(手寫很累的。。。)。因此,咱們這裏能夠利用現有的一些 github 庫,好比 jsonwebtoken。專門用來生成,JWT 的。咱們看一下它顯示的例子:
簡單來講,上面 3 部分都是將對象經過 private key 加密生成的字符串。
info 表明:
{
"typ": "JWT",
"alg": "ES256"
}複製代碼
用來表示 JWT 的加密算法是啥。
Payload 表明:
{
"aud": "https://some-push-service.org",
"exp": "1469618703",
"sub": "mailto:example@web-push-book.org"
}複製代碼
其中
Signature 表明:
它是用來驗證信息安全性的頭。它是前面兩個,JWT.info + '.' + JWT.payload
的字符串經過私有 key 加密的生成的結果。
這就是咱們公鑰的內容,簡單格式爲:
Crypto-Key: dh=<URL Safe Base64 Encoded String>, p256ecdsa=<URL Safe Base64 Public Application Server Key>
// 兩個參數分別表明:
dh=publicKey,p256ecdsa=applicationServerKey複製代碼
這幾個頭是涉及 payload 傳輸時,須要用到的。基本格式爲:
Content-Length: <Number of Bytes in Encrypted Payload> Content-Type: 'application/octet-stream' Content-Encoding: 'aesgcm'複製代碼
其中,只有 Content-Length
是可變的,用來表示 payload 的長度。
這幾個頭上面已經說清楚了,我這裏就不贅述了。
最後放一張關於 SW 的總結圖:
相關推薦
WEB開發性能優化--核心定義介紹篇(1)
頁面性能優化的利器 — Timeline
前端識別驗證碼思路分析
此文已由做者受權騰訊雲技術社區發佈,轉載請註明文章出處
原文連接:www.qcloud.com/community/a…
獲取更多騰訊海量技術實踐乾貨,歡迎你們前往騰訊雲技術社區