最終目的是為了實(shí)現(xiàn)消息推送到用戶設(shè)備并可以顯示更新通知。
實(shí)施推送的三個(gè)關(guān)鍵步驟是:
- 添加客戶端邏輯以訂閱用戶推送(即Web應(yīng)用程序中注冊用戶以推送消息的JavaScript和UI)。
- 來自后端/應(yīng)用程序的API調(diào)用螃壤,觸發(fā)推送消息到用戶的設(shè)備迅矛。
- 服務(wù)工作者JavaScript文件,當(dāng)推送到達(dá)設(shè)備時(shí)將收到“推送事件”。在這個(gè)JavaScript中犬绒,您將能夠顯示通知檩禾。
第一步是“訂閱”用戶推送消息挂签。
訂閱用戶需要兩件事。
- 首先盼产,獲得用戶的許可以向他們發(fā)送推送消息饵婆。
- 第二,然后PushSubscription從瀏覽器中獲取戏售。
PushSubscription包含向該用戶發(fā)送推送消息所需的所有信息侨核。您可以“將”視為該用戶設(shè)備的ID。
特征檢測
首先灌灾,我們需要檢查當(dāng)前瀏覽器是否實(shí)際支持推送消息搓译。我們可以通過兩個(gè)簡單的檢查來檢查是否支持推送。
- 在導(dǎo)航器上檢查serviceWorker紧卒。
- 檢查PushManager的窗口侥衬。
if (!('serviceWorker' in navigator)) {
// Service Worker isn't supported on this browser, disable or hide UI.
return;
}
if (!('PushManager' in window)) {
// Push isn't supported on this browser, disable or hide UI.
return;
}
注冊服務(wù)工作者
當(dāng)我們注冊服務(wù)工作者時(shí),我們告訴瀏覽器我們的服務(wù)工作者文件在哪里跑芳。該文件仍然只是JavaScript轴总,但瀏覽器將“授予它訪問”服務(wù)工作者API,包括推送博个。
更確切地說怀樟,瀏覽器在服務(wù)工作者環(huán)境中運(yùn)行該文件。
要注冊服務(wù)工作者盆佣,請調(diào)用navigator.serviceWorker.register()往堡,將路徑傳遞給我們的文件
function registerServiceWorker() {
return navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
console.log('Service worker successfully registered.');
return registration;
})
.catch(function(err) {
console.error('Unable to register service worker.', err);
});
}
- 下載服務(wù)工作文件械荷。
- 運(yùn)行JavaScript。
- 如果一切正常并且沒有錯(cuò)誤虑灰,則返回的承諾register() 將解決吨瞎。如果有任何類型的錯(cuò)誤,承諾將拒絕穆咐。
register()
時(shí)候颤诀,它返回一個(gè)ServiceWorkerRegistration
。我們將使用此來訪問PushManager API对湃。
請求許可
我們已經(jīng)注冊了我們的服務(wù)工作者并準(zhǔn)備訂閱用戶崖叫,下一步是獲得用戶的許可以向他們發(fā)送推送消息。
獲取權(quán)限的API相對(duì)簡單拍柒,缺點(diǎn)是API 最近從回調(diào)變?yōu)榉祷豍romise心傀。問題在于,我們無法分辨當(dāng)前瀏覽器實(shí)現(xiàn)的API版本拆讯,因此您必須實(shí)現(xiàn)這兩個(gè)版本并同時(shí)處理這兩個(gè)版本脂男。
function askPermission() {
return new Promise(function(resolve, reject) {
const permissionResult = Notification.requestPermission(function(result) {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
}
})
.then(function(permissionResult) {
if (permissionResult !== 'granted') {
throw new Error('We weren\'t granted permission.');
}
});
}
Notification.requestPermission()。此方法將向用戶顯示提示:
一旦許可被接受/允許种呐,關(guān)閉(即點(diǎn)擊彈出窗口上的十字架)或被阻止疆液,我們將以字符串形式給出結(jié)果:'授權(quán)','默認(rèn)'或'拒絕'陕贮。(granted, denied, or default.)
在上面的示例代碼中堕油,askPermission()如果授予了權(quán)限,則通過解析返回的promise 肮之,否則我們會(huì)拋出一個(gè)錯(cuò)誤掉缺,使得promise被拒絕。
您需要處理的一個(gè)邊緣情況是用戶單擊“阻止”按鈕戈擒。如果發(fā)生這種情況眶明,您的網(wǎng)絡(luò)應(yīng)用將無法再次要求用戶獲得許可。他們必須通過更改其權(quán)限狀態(tài)來手動(dòng)“取消阻止”您的應(yīng)用筐高,該權(quán)限狀態(tài)隱藏在設(shè)置面板中搜囱。仔細(xì)考慮如何以及何時(shí)向用戶請求許可,因?yàn)槿绻麄凕c(diǎn)擊阻止柑土,則不是一種簡單的方法來反轉(zhuǎn)該決定蜀肘。
好消息是,大多數(shù)用戶都樂于給予許可稽屏,只要他們知道為什么要求許可扮宠。
使用PushManager訂閱用戶
function subscribeUserToPush() {
return navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
)
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function(pushSubscription) {
console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
return pushSubscription;
});
}
- 您的Web應(yīng)用程序已加載到瀏覽器中,您可以調(diào)用subscribe()狐榔,傳入公共應(yīng)用程序服務(wù)器密鑰坛增。
- 然后获雕,瀏覽器向推送服務(wù)發(fā)出網(wǎng)絡(luò)請求,推送服務(wù)將生成端點(diǎn)收捣,將此端點(diǎn)與應(yīng)用程序公鑰相關(guān)聯(lián)届案,并將端點(diǎn)返回到瀏覽器。
- 瀏覽器會(huì)將此端點(diǎn)添加到PushSubscription,通過subscribe()promise 返回的 端點(diǎn)罢艾。
在調(diào)用subscribe()方法時(shí)萝玷,我們傳入一個(gè)options對(duì)象,它包含必需參數(shù)和可選參數(shù)昆婿。
當(dāng)您以后想要發(fā)送推送消息時(shí),您需要?jiǎng)?chuàng)建一個(gè)Authorization標(biāo)頭蜓斧,其中包含使用您的應(yīng)用程序服務(wù)器的私鑰簽名的信息仓蛆。當(dāng)推送服務(wù)接收到發(fā)送推送消息的請求時(shí),它可以通過查找鏈接到接收請求的端點(diǎn)的公鑰來驗(yàn)證該簽名的授權(quán)報(bào)頭挎春。如果簽名有效看疙,則推送服務(wù)知道它必須來自具有匹配私鑰的應(yīng)用服務(wù)器 。它基本上是一種安全措施直奋,可以防止其他人向應(yīng)用程序的用戶發(fā)送消息能庆。
PushSubscription對(duì)象包含向該用戶發(fā)送推送消息所需的所有必需信息
{
"endpoint": "https://some.pushservice.com/something-unique",
"keys": {
"p256dh":
"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
"auth":"FPssNDTKnInHVndSTdbKFw=="
}
}
這endpoint是推送服務(wù)URL。要觸發(fā)推送消息脚线,請對(duì)此URL發(fā)出POST請求搁胆。
該keys對(duì)象包含用于加密通過推送消息發(fā)送的消息數(shù)據(jù)的值
userVisibleOnly選項(xiàng)
當(dāng)推送首次添加到瀏覽器時(shí),開發(fā)人員是否應(yīng)該能夠發(fā)送推送消息而不顯示通知存在不確定性邮绿。這通常被稱為靜默推送渠旁,因?yàn)橛脩舨恢涝诤笈_(tái)發(fā)生了某些事情。
令人擔(dān)憂的是船逮,開發(fā)人員可能會(huì)做一些討厭的事情顾腊,例如在用戶不知情的情況下持續(xù)跟蹤用戶的位置。
為了避免這種情況并讓規(guī)范作者有時(shí)間考慮如何最好地支持此功能挖胃,userVisibleOnly添加了該選項(xiàng)并且傳入值true是與瀏覽器的符號(hào)協(xié)議杂靶,每次收到推送時(shí)Web應(yīng)用程序都會(huì)顯示通知(即沒有沉默的推動(dòng))。
目前你必須傳入一個(gè)值true酱鸭。如果您不包含 userVisibleOnly密鑰或傳入吗垮,false您將收到以下錯(cuò)誤:
Chrome目前僅支持用于訂閱的Push API,這將導(dǎo)致用戶可見的消息凹髓。你可以通過調(diào)用
pushManager.subscribe({userVisibleOnly: true})
來表明這一點(diǎn) 抱既。有關(guān)詳細(xì)信息,請參閱https://goo.gl/yqv4Q4扁誓。
applicationServerKey選項(xiàng)
推送服務(wù)使用“應(yīng)用服務(wù)器密鑰”來標(biāo)識(shí)訂閱用戶的應(yīng)用程序防泵,并確保相同的應(yīng)用程序正在向該用戶發(fā)送消息蚀之。
應(yīng)用程序服務(wù)器密鑰是公鑰和私鑰對(duì),對(duì)您的應(yīng)用程序而言是唯一的捷泞。私鑰應(yīng)該對(duì)您的應(yīng)用程序保密足删,公鑰可以自由共享。
applicationServerKey傳遞給subscribe()調(diào)用的選項(xiàng)是應(yīng)用程序的公鑰锁右。在訂閱用戶時(shí)失受,瀏覽器將此傳遞到推送服務(wù),這意味著推送服務(wù)可以將應(yīng)用程序的公鑰綁定到用戶的PushSubscription咏瑟。
您可以通過訪問web-push-codelab.glitch.me來創(chuàng)建公共和私有應(yīng)用程序服務(wù)器密鑰集拂到, 也可以 通過執(zhí)行以下操作使用web-push命令行生成密鑰
$ npm install -g web-push
$ web-push generate-vapid-keys
將訂閱發(fā)送到您的服務(wù)器
const subscriptionObject = {
endpoint: pushSubscription.endpoint,
keys: {
p256dh: pushSubscription.getKeys('p256dh'),
auth: pushSubscription.getKeys('auth')
}
};
// The above is the same output as:
const subscriptionObjectToo = JSON.stringify(pushSubscription);
function sendSubscriptionToBackEnd(subscription) {
return fetch('/api/save-subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.then(function(response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function(responseData) {
if (!(responseData.data && responseData.data.success)) {
throw new Error('Bad response from server.');
}
});
}
節(jié)點(diǎn)服務(wù)器接收此請求并將數(shù)據(jù)保存到數(shù)據(jù)庫以供以后使用。
app.post('/api/save-subscription/', function (req, res) {
if (!isValidSaveRequest(req, res)) {
return;
}
return saveSubscriptionToDatabase(req.body)
.then(function(subscriptionId) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ data: { success: true } }));
})
.catch(function(err) {
res.status(500);
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({
error: {
id: 'unable-to-save-subscription',
message: 'The subscription was received but we were unable to save it to our database.'
}
}));
});
});
使用Web推送庫發(fā)送消息
保存訂閱
使用網(wǎng)絡(luò)推送時(shí)的一個(gè)難點(diǎn)是觸發(fā)推送消息非陈肱ⅲ“繁瑣”兄旬。要觸發(fā)推送消息,應(yīng)用程序需要按照Web推送協(xié)議向推送服務(wù)發(fā)出POST請求余寥。要在所有瀏覽器中使用push领铐,您需要使用VAPID (也稱為應(yīng)用程序服務(wù)器密鑰),這基本上需要設(shè)置一個(gè)標(biāo)頭宋舷,其值證明您的應(yīng)用程序可以向用戶發(fā)送消息绪撵。要使用推送消息發(fā)送數(shù)據(jù),需要對(duì)數(shù)據(jù)進(jìn)行 加密并添加特定標(biāo)頭祝蝠,以便瀏覽器可以正確解密消息音诈。
觸發(fā)推送的主要問題是,如果遇到問題绎狭,很難診斷問題改艇。隨著時(shí)間的推移和瀏覽器的廣泛支持,這種情況正在改善坟岔,但這并不容易谒兄。
我們將使用web-push節(jié)點(diǎn)庫。
這個(gè)演示使用nedb存儲(chǔ)訂閱社付,它是一個(gè)簡單的基于文件的數(shù)據(jù)庫承疲,但您可以使用您選擇的任何數(shù)據(jù)庫。我們只使用它鸥咖,因?yàn)樗枰阍O(shè)置燕鸽。對(duì)于生產(chǎn),你想要使用更可靠的東西啼辣。(我傾向于堅(jiān)持使用舊的MySQL啊研。)
function saveSubscriptionToDatabase(subscription) {
return new Promise(function(resolve, reject) {
db.insert(subscription, function(err, newDoc) {
if (err) {
reject(err);
return;
}
resolve(newDoc._id);
});
});
};
發(fā)送推送消息
在發(fā)送推送消息時(shí),我們最終需要一些事件來觸發(fā)向用戶發(fā)送消息的過程。一種常見的方法是創(chuàng)建一個(gè)管理頁面党远,讓您配置并觸發(fā)推送消息削解。但是您可以創(chuàng)建一個(gè)在本地運(yùn)行的程序或任何其他允許訪問PushSubscriptions代碼列表并運(yùn)行代碼以觸發(fā)推送消息的方法。
接下來我們需要web-push為我們的Node服務(wù)器安裝模塊:
npm install web-push --save
然后在我們的Node腳本中沟娱,我們需要在web-push模塊中這樣:
const webpush = require('web-push');
首先氛驮,我們需要告訴web-push模塊我們的應(yīng)用服務(wù)器密鑰
const vapidKeys = {
publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls'
};
webpush.setVapidDetails(
'mailto:web-push-book@gauntface.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
我們還包括一個(gè)“mailto:”字符串。此字符串必須是URL或mailto電子郵件地址济似。這條信息實(shí)際上將作為觸發(fā)推送請求的一部分發(fā)送到Web推送服務(wù)矫废。這樣做的原因是,如果網(wǎng)絡(luò)推送服務(wù)需要與發(fā)送者聯(lián)系砰蠢,他們會(huì)有一些信息可以讓他們這樣做蓖扑。
有了這個(gè),web-push模塊就可以使用了台舱,下一步是觸發(fā)推送消息律杠。
該演示使用假裝管理面板來觸發(fā)推送消息。
單擊“觸發(fā)推送消息”按鈕將發(fā)出POST請求柿赊,/api/trigger-push-msg/ 這是我們后端發(fā)送推送消息的信號(hào),因此我們?yōu)榇硕它c(diǎn)創(chuàng)建快速路由:
app.post('/api/trigger-push-msg/', function (req, res) {
收到此請求后幻枉,我們從數(shù)據(jù)庫中獲取訂閱碰声,對(duì)于每個(gè)訂閱,我們會(huì)觸發(fā)推送消息熬甫。
return getSubscriptionsFromDatabase()
.then(function(subscriptions) {
let promiseChain = Promise.resolve();
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i];
promiseChain = promiseChain.then(() => {
return triggerPushMsg(subscription, dataToSend);
});
}
return promiseChain;
})
triggerPushMsg()然后胰挑,該函數(shù)可以使用Web推送庫向提供的訂閱發(fā)送消息。
const triggerPushMsg = function(subscription, dataToSend) {
return webpush.sendNotification(subscription, dataToSend)
.catch((err) => {
if (err.statusCode === 410) {
return deleteSubscriptionFromDatabase(subscription._id);
} else {
console.log('Subscription is no longer valid: ', err);
}
});
};
webpush.sendNotification()將返回一個(gè)承諾椿肩。如果消息已成功發(fā)送瞻颂,則承諾將解決,我們無需執(zhí)行任何操作郑象。如果承諾拒絕嘶是,您需要檢查錯(cuò)誤巩割,因?yàn)樗鼤?huì)告知您PushSubscription是否仍然有效。
要確定推送服務(wù)的錯(cuò)誤類型,最好查看狀態(tài)代碼矾湃。推送服務(wù)之間的錯(cuò)誤消息不同,有些比其他更有幫助匹耕。
在此示例中烛缔,它檢查狀態(tài)代碼'404'和'410',它們是'Not Found'和'Gone'的HTTP狀態(tài)代碼柜砾。如果我們收到其中一個(gè)湃望,則表示訂閱已過期或不再有效。在這些場景中,我們需要從數(shù)據(jù)庫中刪除訂閱证芭。
處理推送事件
在用戶的設(shè)備上接收此推送消息并顯示通知
收到消息后瞳浦,它將導(dǎo)致在服務(wù)工作者中調(diào)度推送事件。
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.');
}
});
self通常用于服務(wù)工作者的Web Workers檩帐。self指的是全局范圍术幔,類似于window網(wǎng)頁。但對(duì)于網(wǎng)絡(luò)工作者和服務(wù)工作者來說湃密, self指的是工作者本身诅挑。
self.addEventListener()可以將其視為向服務(wù)工作者本身添加事件偵聽器。
// Returns string
event.data.text()
// Parses data as JSON string and returns an Object
event.data.json()
// Returns blob of data
event.data.blob()
// Returns an arrayBuffer
event.data.arrayBuffer()
服務(wù)工作者需要了解的一點(diǎn)是泛源,您幾乎無法控制服務(wù)工作者代碼何時(shí)運(yùn)行拔妥。瀏覽器決定何時(shí)將其喚醒以及何時(shí)終止它。你可以告訴瀏覽器的唯一方法是“嘿达箍,我忙著做重要的事情”没龙,就是將一個(gè)承諾傳遞給event.waitUntil()方法。有了這個(gè)缎玫,瀏覽器將保持服務(wù)工作者運(yùn)行硬纤,直到您傳入的承諾結(jié)束。
對(duì)于推送事件赃磨,還有一個(gè)額外要求筝家,即您必須在傳遞的承諾結(jié)算之前顯示通知。
self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
event.waitUntil(promiseChain);
});
調(diào)用self.registration.showNotification()是向用戶顯示通知的方法邻辉,它返回一個(gè)在顯示通知后將解析的承諾溪王。
通過網(wǎng)絡(luò)數(shù)據(jù)請求和使用分析跟蹤推送事件的更復(fù)雜示例可能如下所示:
self.addEventListener('push', function(event) {
const analyticsPromise = pushReceivedTracking();
const pushInfoPromise = fetch('/api/get-more-data')
.then(function(response) {
return response.json();
})
.then(function(response) {
const title = response.data.userName + ' says...';
const message = response.data.message;
return self.registration.showNotification(title, {
body: message
});
});
const promiseChain = Promise.all([
analyticsPromise,
pushInfoPromise
]);
event.waitUntil(promiseChain);
});
這里我們調(diào)用一個(gè)返回promise的函數(shù),pushReceivedTracking()為了示例值骇,我們可以假裝向我們的分析提供者發(fā)出網(wǎng)絡(luò)請求莹菱。我們還發(fā)出網(wǎng)絡(luò)請求,獲取響應(yīng)并使用響應(yīng)數(shù)據(jù)顯示通知的標(biāo)題和消息吱瘩。
我們可以確保服務(wù)工作者保持活力道伟,同時(shí)通過將這些承諾與這些承諾相結(jié)合來完成這兩項(xiàng)任務(wù)Promise.all()。產(chǎn)生的承諾將被轉(zhuǎn)換為event.waitUntil() 意味著瀏覽器將等待兩個(gè)承諾完成使碾,然后再檢查是否已顯示通知并終止服務(wù)工作者皱卓。
我們應(yīng)該關(guān)注waitUntil()以及如何使用它的原因是開發(fā)人員面臨的最常見問題之一是,當(dāng)承諾鏈不正確/損壞時(shí)部逮,Chrome會(huì)顯示此“默認(rèn)”通知:
Chrome只會(huì)顯示“此網(wǎng)站已在后臺(tái)更新”娜汁。收到推送消息時(shí)的通知,并且服務(wù)工作者中的推送事件在傳遞到的承諾event.waitUntil()完成后未顯示通知兄朋。
顯示通知
顯示通知的API是:
<ServiceWorkerRegistration>.showNotification(<title>, <options>);
標(biāo)題是字符串掐禁,選項(xiàng)可以是以下任何一種:
{
"http://": "Visual Options",
"body": "<String>",
"icon": "<URL String>",
"image": "<URL String>",
"badge": "<URL String>",
"vibrate": "<Array of Integers>",
"sound": "<URL String>",
"dir": "<String of 'auto' | 'ltr' | 'rtl'>",
"http://": "Behavioural Options",
"tag": "<String>",
"data": "<Anything>",
"requireInteraction": "<boolean>",
"renotify": "<Boolean>",
"silent": "<Boolean>",
"http://": "Both Visual & Behavioural Options",
"actions": "<Array of Strings>",
"http://": "Information Option. No visual affect.",
"timestamp": "<Long>"
}
瀏覽器支持:
https://jakearchibald.github.io/isserviceworkerready/