#Push API指南
推送API允許網絡應用程序接收由服務器推送的消息,即使該Web應用程序在瀏覽器中沒有打開或者不在設備上運行。
推送API允許網絡應用程序接收由服務器推送的消息,即使該Web應用程序在瀏覽器中沒有打開或者不在設備上運行。
使用推送API,您可以向用戶發送消息,從服務器推送到客戶端,即使用戶未在瀏覽站點。
這使您能夠提供通知和內容更新,從而使受眾更加參與。
這是巨大的,因為與本機應用程序相比,移動網絡的一個缺失的基礎是能夠接收通知的能力,以及離線支持。
是否受到支持?
推送API是瀏覽器API的最新添加,目前Chrome(桌面和移動平台),Firefox和Opera支持自2016年以來,Edge自17版(2018年初)開始支持。查看有關當前瀏覽器支持狀況的更多信息,請訪問https://caniuse.com/#feat=push-api
IE不支持它,Safari有自己的實現方式。
由於Chrome和Firefox支持它,大約60%的桌面瀏覽器用戶可以使用它,所以使用它是相對較安全的。
它是如何工作的
總覽
當用戶訪問您的Web應用程序時,您可以觸發一個面板要求許可權來發送更新。安裝了服務器工作者(Service Worker),並在後台運行,監聽推送事件(Push Event)。
推送和通知是一個獨立的概念和API,有時由於在iOS中使用“推送通知”詞彙而混淆。基本上,當使用推送API接收到推送事件時,調用通知API。
如果給予權限,您的服務器將通知發送到客戶端,服務工作者(Service Worker)接收到**推送事件(Push Event)**後將對此事件做出反應,觸發通知。
獲取用戶權限
使用推送API的第一步是獲取用戶允許從您接收數據。
許多網站在第一次頁面加載時執行此面板時錯誤,用戶還沒有確定您的內容是否好,他們將拒絕授權。請明智地運用。
有6個步驟:
- 檢查是否支持服務器工作者
- 檢查是否支持推送API
- 註冊服務器工作者
- 向用戶請求權限
- 訂閱用戶並獲取PushSubscription對象
- 將PushSubscription對象發送到您的服務器
檢查是否支持服務器工作者
if (!('serviceWorker' in navigator)) {
// 不支持服務器工作者,返回
return
}
檢查是否支持推送API
if (!('PushManager' in window)) {
// 不支持推送API,返回
return
}
註冊服務器工作者
此代碼註冊位於域根目錄的worker.js
文件中的服務器工作者。
window.addEventListener('load', () => {
navigator.serviceWorker.register('/worker.js')
.then((registration) => {
console.log('服務器工作者註冊完成,範圍:',
registration.scope)
}, (err) => {
console.log('服務器工作者註冊失敗', err)
})
})
要了解有關服務器工作者的詳細信息,請查看服務器工作者指南。
向用戶請求權限
現在,服務器工作者已註冊,您可以請求權限。
這個API隨著時間的推移進行了修改,從接受回調函數作為參數,變為返回一個Promise(承諾),這打破了向前和向後的兼容性,而我們需要兩者都進行,因為我們不知道用戶的瀏覽器實現了哪種方法。
代碼如下,調用Notification.requestPermission()
。
const askPermission = () => {
return new Promise((resolve, reject) => {
const permissionResult = Notification.requestPermission((result) => {
resolve(result)
})
if (permissionResult) {
permissionResult.then(resolve, reject)
}
})
.then((permissionResult) => {
if (permissionResult !== 'granted') {
throw new Error('權限被拒絕')
}
})
}
permissionResult
值是一個字符串,可以有以下值:
granted
default
denied
此代碼導致瀏覽器顯示權限對話框:
如果用戶點擊了“阻止”,則您將無法再次請求用戶的權限,除非他們在瀏覽器的高級設置面板中手動解除對站點的阻止(很不可能發生)。
訂閱用戶並獲取PushSubscription對象
如果用戶給予我們權限,我們可以通過調用registration.pushManager.subscribe()
訂閱用戶。
const APP_SERVER_KEY = 'XXX'
window.addEventListener('load', () => {
navigator.serviceWorker.register('/worker.js')
.then((registration) => {
askPermission().then(() => {
const options = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(APP_SERVER_KEY)
}
return registration.pushManager.subscribe(options)
}).then((pushSubscription) => {
// 獲取到的pushSubscription對象
}
}, (err) => {
console.log('服務器工作者註冊失敗', err)
})
})
APP_SERVER_KEY
是一個字符串,稱為Application Server Key或者VAPID key,它識別應用程序的公共金鑰,作為公共/私有金鑰對的一部分。
出於安全原因的驗證,在確保只有您(而不是其他人)可以向用戶發送推送消息時使用。
將PushSubscription對象發送到您的服務器
在之前的代碼片段中,我們獲取了pushSubscription
對象,其中包含了我們發送推送消息所需的所有信息。我們需要將此信息發送到我們的服務器,以便稍後發送通知。
首先我們創建對象的JSON表示:
const subscription = JSON.stringify(pushSubscription)
然後我們可以使用Fetch API將其發送給我們的服務器:
const sendToServer = (subscription) => {
return fetch('/api/subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.then((res) => {
if (!res.ok) {
throw new Error('發生錯誤')
}
return res.json()
})
.then((resData) => {
if (!(resData.data && resData.data.success)) {
throw new Error('發生錯誤')
}
})
}
sendToServer(subscription)
服務器端如何工作
到目前為止,我們只談到了客戶端的部分:獲取用戶在未來接收通知的權限。
那麼服務器應該做什麼,以及如何與客戶端交互呢?
此服務器端示例使用Express.js (http://expressjs.com/)作為基本的HTTP框架,但您可以使用任何語言或框架編寫服務器端的Push API處理程序
註冊新的用戶端訂閱
當客戶端發送新的訂閱時,請記住我們在/api/subscription
HTTP POST端點上使用了PushSubscription對象的詳細信息,以JSON格式在正文中發送。
我們初始化Express.js:
const express = require('express')
const app = express()
此實用函數確保請求有效,具有正文和端點屬性,否則它將返回一個錯誤給客戶端:
const isValidSaveRequest = (req, res) => {
if (!req.body || !req.body.endpoint) {
res.status(400)
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify({
error: {
id: 'no-endpoint',
message: '訂閱必須有一個終端點'
}
}))
return false
}
return true
}
下一個實用函數將訂閱保存到數據庫中,返回一個承諾,當插入完成時解析(或失敗)。insertToDatabase
函數是一個占位符,我們不會在此詳細討論那些細節:
const saveSubscriptionToDatabase = (subscription) => {
return new Promise((resolve, reject) => {
insertToDatabase(subscription, (err, id) => {
if (err) {
reject(err)
return
}
resolve(id)
})
})
}
我們在POST請求處理程序中使用這些函數。我們檢查請求是否有效,然後保存請求,然後將data.success: true
響應返回給客戶端,或者返回錯誤:
app.post('/api/subscription', (req, res) => {
if (!isValidSaveRequest(req, res)) {
return
}
saveSubscriptionToDatabase(req, res.body)
.then((subscriptionId) => {
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify({ data: { success: true } }))
})
.catch((err) => {
res.status(500)
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify({
error: {
id: 'unable-to-save-subscription',
message: '訂閱收到但保存失敗'
}
}))
})
})
app.listen(3000, () => {
console.log('應用聽取端口3000')
})
發送推送消息
現在服務器在其列表中註冊了客戶端後,我們可以向其發送推送消息。讓我們通過創建一個示例代碼片段來查看它的工作原理,該片段會獲取所有訂閱,並馬上將Push消息推送給它們。
我們使用一個庫,因為Web Push協議非常複雜,庫使我們可以將很多低級代碼抽象出來,以確保我們能夠安全地工作並正確地處理任何邊緣情況。
此示例使用
web-push
Node.js庫(https://github.com/web-push-libs/web-push)來處理推送消息
首先我們初始化web-push
庫,並生成私鑰和公鑰的元組,並將其設置為VAPID的詳細信息:
const webpush = require('web-push')
const vapidKeys = webpush.generateVAPIDKeys()
const PUBLIC_KEY = 'XXX'
const PRIVATE_KEY = 'YYY'
const vapidKeys = {
publicKey: PUBLIC_KEY,
privateKey: PRIVATE_KEY
}
webpush.setVapidDetails(
'mailto:[[email protected]](/cdn-cgi/l/email-protection)',
vapidKeys.publicKey,
vapidKeys.privateKey
)
然後我們設置一個triggerPush()
方法,負責向客戶端發送推送事件。它只需要調用webpush.sendNotification()
並捕獲任何錯誤。如果返回的錯誤HTTP狀態碼是410,表示已消失,我們將從數據庫中刪除該訂閱者。
const triggerPush = (subscription, dataToSend) => {
return webpush.sendNotification(subscription, dataToSend)
.catch((err) => {
if (err.statusCode === 410) {
return deleteSubscriptionFromDatabase(subscription.\_id)
} else {
console.log('訂閱者不再有效:', err)
}
})
}
我們不實現從數據庫中獲取訂閱的功能,但我們將其作為存根:
const getSubscriptionsFromDatabase = () => {
//存根
}
代碼的核心是POST請求的回調函數,該請求發送到/api/push
端點:
app.post('/api/push', (req, res) => {
return getSubscriptionsFromDatabase()
.then((subscriptions) => {
let promiseChain = Promise.resolve()
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i]
promiseChain = promiseChain.then(() => {
return triggerPush(subscription, dataToSend)
})
}
return promiseChain
})
.then(() => {
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify({ data: { success: true } }))
})
.catch((err) => {
res.status(500)
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify({
error: {
id: 'unable-to-send-messages',
message: `無法發送推送 ${err.message}`
}
}))
})
})
上面的代碼的作用是:它從數據庫中獲取所有訂閱,然後對它們進行迭代,並調用我們之前解釋的triggerPush()
函數。
完成訂閱後,我們返回一個成功的JSON響應,除非發生錯誤,我們返回一個500錯誤。
在現實世界中…
您不太可能設置自己的推送服務器,除非您有一個非常特殊的用例,或者只是想學習技術或者喜歡DIY。相反,您通常希望使用類似OneSignal(https://onesignal.com)這樣的平台,它能夠透明地處理各種平台的推送事件,包括Safari和iOS,而且還是免費的。
接收推送事件
當從服務器發送推送事件時,客戶端如何接收它呢?
它是一個正常的[JavaScript(/javascript/)]事件監聽器,在push
事件上運行在服務器工作者(Service Worker)內:
self.addEventListener('push', (event) => {
//數據可以在event.data中獲取
})
event.data
中包含PushMessageData
對象,它公開了方法,以您希望的格式檢索由服務器發送的推送數據:
- arrayBuffer():作為
ArrayBuffer
對象 - blob():作為Blob對象
- json():解析為JSON
- text():純文本
通常我們會使用event.data.json()
。
顯示通知
這裡我們稍微涉及到了通知API,但出於一個好的原因,因為Push API的一個主要用例是顯示通知。
在服務器工作者(Service Worker)中的push
事件監聽器內部,我們需要將通知顯示給用戶,並告訴事件在瀏覽器顯示通知之前等待。我們將事件的生命週期延長到瀏覽器顯示通知之前(直到Promise被解析),否則服務器工作者可能在處理過程中被停止。
self.addEventListener('push', (event) => {
const promiseChain = self.registration.showNotification('嘿!')
event.waitUntil(promiseChain)
})
有關通知的更多信息,請參見通知API指南。