/

總覽

#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個步驟:

  1. 檢查是否支持服務器工作者
  2. 檢查是否支持推送API
  3. 註冊服務器工作者
  4. 向用戶請求權限
  5. 訂閱用戶並獲取PushSubscription對象
  6. 將PushSubscription對象發送到您的服務器

檢查是否支持服務器工作者

1
2
3
4
if (!('serviceWorker' in navigator)) {
// 不支持服務器工作者,返回
return
}

檢查是否支持推送API

1
2
3
4
if (!('PushManager' in window)) {
// 不支持推送API,返回
return
}

註冊服務器工作者

此代碼註冊位於域根目錄的worker.js文件中的服務器工作者。

1
2
3
4
5
6
7
8
9
window.addEventListener('load', () => {
navigator.serviceWorker.register('/worker.js')
.then((registration) => {
console.log('服務器工作者註冊完成,範圍:',
registration.scope)
}, (err) => {
console.log('服務器工作者註冊失敗', err)
})
})

要了解有關服務器工作者的詳細信息,請查看服務器工作者指南

向用戶請求權限

現在,服務器工作者已註冊,您可以請求權限。

這個API隨著時間的推移進行了修改,從接受回調函數作為參數,變為返回一個Promise(承諾),這打破了向前和向後的兼容性,而我們需要兩者都進行,因為我們不知道用戶的瀏覽器實現了哪種方法。

代碼如下,調用Notification.requestPermission()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()訂閱用戶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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表示:

1
const subscription = JSON.stringify(pushSubscription)

然後我們可以使用Fetch API將其發送給我們的服務器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

1
2
const express = require('express')
const app = express()

此實用函數確保請求有效,具有正文和端點屬性,否則它將返回一個錯誤給客戶端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函數是一個占位符,我們不會在此詳細討論那些細節:

1
2
3
4
5
6
7
8
9
10
11
12
const saveSubscriptionToDatabase = (subscription) => {
return new Promise((resolve, reject) => {
insertToDatabase(subscription, (err, id) => {
if (err) {
reject(err)
return
}

resolve(id)
})
})
}

我們在POST請求處理程序中使用這些函數。我們檢查請求是否有效,然後保存請求,然後將data.success: true響應返回給客戶端,或者返回錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的詳細信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,表示已消失,我們將從數據庫中刪除該訂閱者。

1
2
3
4
5
6
7
8
9
10
const triggerPush = (subscription, dataToSend) => {
return webpush.sendNotification(subscription, dataToSend)
.catch((err) => {
if (err.statusCode === 410) {
return deleteSubscriptionFromDatabase(subscription.\_id)
} else {
console.log('訂閱者不再有效:', err)
}
})
}

我們不實現從數據庫中獲取訂閱的功能,但我們將其作為存根:

1
2
3
const getSubscriptionsFromDatabase = () => {
//存根
}

代碼的核心是POST請求的回調函數,該請求發送到/api/push端點:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)內:

1
2
3
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被解析),否則服務器工作者可能在處理過程中被停止。

1
2
3
4
self.addEventListener('push', (event) => {
const promiseChain = self.registration.showNotification('嘿!')
event.waitUntil(promiseChain)
})

有關通知的更多信息,請參見通知API指南