第一步是取得使用者同意,以便向他們傳送推送訊息,接著我們就能實作 PushSubscription
。
要利用 JavaScript API 執行此操作,基本上是直觀的,因此我們要逐步瞭解邏輯流程。
功能偵測
首先,我們需要檢查目前的瀏覽器是否確實支援推送訊息功能。我們可透過兩個簡單的檢查來確認系統是否支援推送。
- 在 navigator 上檢查 serviceWorker。
- 檢查 window 上的 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;
}
儘管服務工作站和推送訊息的瀏覽器支援正在迅速增加,還是建議您針對功能和漸進式強化進行功能偵測。
註冊 Service Worker
功能偵測表示 Service Worker 和推送作業都受到支援。下一步是「註冊」Service Worker,
註冊 Service Worker 時,系統會向瀏覽器告知 Service Worker 檔案的位置。這個檔案仍然只是 JavaScript,但瀏覽器會「授予存取權」給 Service Worker API,包括推送。更明確地說,瀏覽器會在 Service Worker 環境中執行檔案。
如要註冊 Service Worker,請呼叫 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);
});
}
這個函式會告訴瀏覽器我們有一個 Service Worker 檔案,以及檔案的所在位置。在這個情況下,Service Worker 檔案位於 /service-worker.js
。呼叫 register()
後,瀏覽器會在背景執行下列步驟:
下載 Service Worker 檔案。
執行 JavaScript。
如果所有項目都能正確執行,且未發生任何錯誤,
register()
傳回的 promise 就會解決問題。如果出現任何類型的錯誤,承諾會遭到拒絕。
如果
register()
拒絕,請仔細檢查 Chrome 開發人員工具中的 JavaScript,檢查是否有錯字或錯誤。
當 register()
解析時,會傳回 ServiceWorkerRegistration
。我們會使用這項註冊作業存取 PushManager API。
PushManager API 瀏覽器相容性
要求權限
我們已註冊服務工作處理程序,並準備好訂閱使用者了,下一步是取得使用者授予傳送推送訊息的權限。
用於取得權限的 API 相對簡單,缺點則是 API 最近從回呼改為傳回 Promise。但這個問題是無法判斷目前瀏覽器實作的 API 版本,因此您必須實作並處理這兩個版本。
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()
的呼叫。這個方法會向使用者顯示提示:
當使用者按下「允許」、「封鎖」或直接關閉權限提示互動後,系統會以字串的形式提供結果:'granted'
、'default'
或 'denied'
。
在上述程式碼範例中,askPermission()
傳回的承諾會在授予權限後解決,否則系統會擲回錯誤,導致 promise 拒絕。
需要處理的極端案例是使用者點選「封鎖」按鈕。在這種情況下,網頁應用程式將無法再次要求使用者授予權限。他們必須變更應用程式的權限狀態 (在設定面板中隱藏),手動「解除封鎖」應用程式。請仔細思考您向使用者要求權限的方式和時機,因為如果對方點選「封鎖」,要撤銷該項決定並不容易。
好消息是,只要使用者知道系統要求權限的原因,通常都能獲得權限。
我們稍後會說明部分熱門網站如何要求權限。
透過 PushManager 為使用者訂閱
註冊服務工作處理程序且取得權限後,即可呼叫 registration.pushManager.subscribe()
來訂閱使用者。
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;
});
}
呼叫 subscribe()
方法時,我們會傳入 options 物件,該物件由必要和選用參數組成。
接著來看看可以傳入的所有選項。
userVisibleOnly 選項
推送首次新增到瀏覽器時,不確定開發人員是否應傳送推送訊息,以及未顯示通知。這通常稱為無聲推送,因為使用者不知道在背景發生了什麼事。
開發人員的問題在於,開發人員可以在不知情的情況下,持續追蹤使用者的位置,例如追蹤使用者的位置。
為了避免這種情況,並讓規格作者有時間考慮如何支援這項功能,已新增 userVisibleOnly
選項,並傳入 true
值是與瀏覽器之間的符號協議,每當收到推送推送時,網頁應用程式都會顯示通知 (即沒有靜音推送)。
目前,您必須傳入 true
值。如未加入 userVisibleOnly
鍵或傳入 false
,就會收到下列錯誤訊息:
Chrome 目前僅支援 Push API,用於將引發使用者可見訊息的訂閱項目。您可以改為呼叫 pushManager.subscribe({userVisibleOnly: true})
來表示。詳情請參閱 https://goo.gl/yqv4Q4。
因為目前 Chrome 一律無法實作全面無聲推送功能。相反地,規格作者會探索預算 API 的概念,這個 API 將根據網頁應用程式的使用情形,允許網頁應用程式獲得特定數量的靜音推送訊息。
applicationServerKey 選項
上一節已簡單提過「應用程式伺服器金鑰」。推送服務會使用「應用程式伺服器金鑰」來識別訂閱使用者的應用程式,並確保相同的應用程式可以傳送訊息給該使用者。
應用程式伺服器金鑰是應用程式專屬的公開與私密金鑰組。私密金鑰應妥善保存在應用程式內,而且公開金鑰可以自由共用。
傳入 subscribe()
呼叫的 applicationServerKey
選項是應用程式的公開金鑰。瀏覽器會在使用者訂閱時,將其傳送至推送服務,這表示推送服務可以將應用程式的公開金鑰連結至使用者的 PushSubscription
。
下圖說明這些步驟。
- 系統會在瀏覽器中載入網頁應用程式,並呼叫
subscribe()
,並傳入公開應用程式伺服器金鑰。 - 瀏覽器接著向推送服務發出網路要求,而服務會產生端點,將這個端點與應用程式公開金鑰建立關聯,並將端點傳回瀏覽器。
- 瀏覽器會將這個端點新增至
PushSubscription
,並透過subscribe()
承諾傳回。
當您之後想傳送推送訊息時,則需要建立 Authorization 標頭,其中包含以應用程式伺服器私密金鑰簽署的資訊。當推送服務收到傳送推送訊息的要求時,會查詢連結至接收要求的端點的公開金鑰,藉此驗證此已簽署的 Authorization 標頭。如果簽章有效,推送服務就能知道其必須透過相符的私密金鑰來自應用程式伺服器。基本上,這是一項安全措施,可防止他人傳送訊息給應用程式的使用者。
嚴格說來,applicationServerKey
為選用項目。不過,在 Chrome 中採用最簡單的實作方式,未來也可能會有其他瀏覽器要求使用。在 Firefox 中是選擇性的。
定義應用程式伺服器金鑰「什麼」的規格是 VAPID 規格。當您讀取提及「應用程式伺服器金鑰」或「VAPID 金鑰」的內容時,請記得兩者相同。
如何建立應用程式伺服器金鑰
您可以透過 web-push-codelab.glitch.me 建立一組公開和私密的應用程式伺服器金鑰,也可以使用 web-push 指令列產生金鑰,步驟如下:
$ npm install -g web-push
$ web-push generate-vapid-keys
您只需要為應用程式建立這些金鑰一次,只要確保私密金鑰的私密性即可。(沒錯,我剛說了)。
權限和 subscription()
呼叫 subscribe()
有一項副作用。如果網頁應用程式沒有在呼叫 subscribe()
時顯示通知的權限,瀏覽器會為您要求權限。如果您的 UI 能與此流程搭配運作,這個做法就很實用,但如果您想要進一步掌控 (我認為大多數開發人員也會這麼做),請繼續使用我們稍早使用的 Notification.requestPermission()
API。
什麼是 PushSubscription?
我們會呼叫 subscribe()
並傳入部分選項,然後會傳回承諾會解析為 PushSubscription
,進而產生一些程式碼,如下所示:
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;
});
}
PushSubscription
物件包含向該使用者傳送推送訊息所需的所有必要資訊。如果使用 JSON.stringify()
列印內容,您會看到以下內容:
{
"endpoint": "https://some.pushservice.com/something-unique",
"keys": {
"p256dh":
"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
"auth":"FPssNDTKnInHVndSTdbKFw=="
}
}
endpoint
是推送服務網址。如要觸發推送訊息,請向這個網址發出 POST 要求。
keys
物件包含的值會用來加密透過推送訊息傳送的訊息資料 (本節稍後會說明)。
定期重新訂閱,以免過期
訂閱推播通知時,你通常會收到 null
的 PushSubscription.expirationTime
。理論上,這表示訂閱項目永遠不會到期 (與收到 DOMHighResTimeStamp
時相比,您可得知訂閱項目到期的確切時間點)。但在實務上,瀏覽器仍然常會讓訂閱項目過期,例如,如果長時間未收到任何推播通知,或是瀏覽器偵測到使用者並未使用具備推播通知權限的應用程式,就會發生這種情況。防止這種情況發生,其中一個模式就是在使用者每次收到通知時重新訂閱,如以下程式碼片段所示。這需要您經常傳送通知,讓瀏覽器不會自動過期訂閱。此外,在執行正當通知操作時,請務必審慎權衡合理通知使用者的需求,以免訂閱過期。畢竟,請不要為了保護使用者免於長時間忘記通知訂閱的侵擾,而讓瀏覽器更臻完善。
/* In the Service Worker. */
self.addEventListener('push', function(event) {
console.log('Received a push message', event);
// Display notification or handle data
// Example: show a notification
const title = 'New Notification';
const body = 'You have new updates!';
const icon = '/images/icon.png';
const tag = 'simple-push-demo-notification-tag';
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
})
);
// Attempt to resubscribe after receiving a notification
event.waitUntil(resubscribeToPush());
});
function resubscribeToPush() {
return self.registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
})
.then(function() {
return self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
});
})
.then(function(subscription) {
console.log('Resubscribed to push notifications:', subscription);
// Optionally, send new subscription details to your server
})
.catch(function(error) {
console.error('Failed to resubscribe:', error);
});
}
將訂閱項目傳送至您的伺服器
建立推送訂閱項目後,請傳送至您的伺服器。請決定如何執行,但有一個簡單的訣竅是使用 JSON.stringify()
,將所有必要資料從訂閱物件中取得。或者,您也可以手動拼湊同一個結果,如下所示:
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.');
}
});
}
節點伺服器會收到這項要求,並將資料儲存到資料庫,供日後使用。
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.',
},
}),
);
});
});
透過伺服器上的 PushSubscription
詳細資料,我們隨時都能向使用者傳送訊息。
定期重新訂閱,以免過期
訂閱推播通知時,你通常會收到 null
的 PushSubscription.expirationTime
。理論上,這表示訂閱項目永遠不會到期 (與收到 DOMHighResTimeStamp
時相比,您可得知訂閱項目到期的確切時間點)。但在實務上,瀏覽器仍然常會讓訂閱項目過期,例如:長時間未收到任何推播通知,或是瀏覽器偵測到使用者並未使用具備推播通知權限的應用程式。防止這種情況發生,其中一個模式就是在使用者每次收到通知時重新訂閱,如以下程式碼片段所示。這需要您頻繁傳送通知,讓瀏覽器不會自動過期訂閱。此外,請審慎衡量合法通知對於使用者濫發垃圾內容的優點和缺點,以免訂閱過期。畢竟,請不要為了保護使用者免於長時間忘記通知訂閱的侵擾,而讓瀏覽器更臻完善。
/* In the Service Worker. */
self.addEventListener('push', function(event) {
console.log('Received a push message', event);
// Display notification or handle data
// Example: show a notification
const title = 'New Notification';
const body = 'You have new updates!';
const icon = '/images/icon.png';
const tag = 'simple-push-demo-notification-tag';
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
})
);
// Attempt to resubscribe after receiving a notification
event.waitUntil(resubscribeToPush());
});
function resubscribeToPush() {
return self.registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
})
.then(function() {
return self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
});
})
.then(function(subscription) {
console.log('Resubscribed to push notifications:', subscription);
// Optionally, send new subscription details to your server
})
.catch(function(error) {
console.error('Failed to resubscribe:', error);
});
}
常見問題
當前使用者常提出的幾個常見問題:
我可以變更瀏覽器使用的推送服務嗎?
不會。瀏覽器會選取推送服務,而如同 subscribe()
呼叫所示,瀏覽器會向推送服務發出網路要求,以擷取組成 PushSubscription 詳細資料。
每個瀏覽器使用不同的推送服務,是否都擁有不同的 API?
所有推送服務都會預期相同的 API。
這個通用 API 稱為網路推播通訊協定,其中說明應用程式必須提出的網路要求,才能觸發推送訊息。
如果我透過電腦訂閱頻道,使用者會在手機上訂閱嗎?
很抱歉,不行。使用者必須在想接收訊息的每個瀏覽器上進行註冊,才能接收訊息。另外值得注意的是,此情況需要使用者在每部裝置上授予權限。