js13kGames:使用通知和推送 API 讓 PWA 重新吸引使用者
快取應用程式內容以離線工作是一項很棒的功能。允許使用者在他們的裝置上安裝 Web 應用則更佳。但除了僅依賴使用者操作之外,我們還可以做得更多,透過使用推送訊息和通知,在有新內容可用時自動重新吸引使用者並提供新內容。
兩個 API,一個目標
推送 API 和 通知 API 是兩個獨立的 API,但當您想在您的應用程式中提供引人入勝的功能時,它們可以很好地協同工作。推送用於在沒有任何客戶端干預的情況下將新內容從伺服器傳遞到應用程式,其操作由應用程式的服務工作執行緒處理。通知可以由服務工作執行緒用來向用戶顯示新資訊,或者至少在有內容更新時提醒他們。
它們與服務工作執行緒一樣,在瀏覽器視窗外部執行,因此即使應用程式頁面未獲得焦點或已關閉,也可以推送更新並顯示通知。
通知
讓我們先從通知開始——它們可以獨立工作,但與推送結合後會更有用。我們先單獨看看通知。
請求許可權
要顯示通知,我們必須先請求許可權。但最佳實踐表明,我們不應立即顯示通知,而應在使用者透過單擊按鈕請求時顯示彈出視窗。
const button = document.getElementById("notifications");
button.addEventListener("click", () => {
Notification.requestPermission().then((result) => {
if (result === "granted") {
randomNotification();
}
});
});
這會顯示一個使用作業系統自身通知服務的彈出視窗。

當用戶確認接收通知時,應用程式就可以顯示它們了。使用者操作的結果可以是預設、允許或拒絕。當用戶未做出選擇時,將選擇預設選項,而當用戶分別單擊是或否時,則分別設定其他兩個選項。
接受後,許可權對通知和推送都有效。
建立通知
示例應用程式根據可用資料建立通知——隨機選擇一個遊戲,然後將選定的遊戲內容用於通知:它將遊戲名稱設定為標題,在正文中提及作者,並將影像顯示為圖示。
function randomNotification() {
if (!swRegistration) return;
const randomItem = Math.floor(Math.random() * games.length);
const notifTitle = games[randomItem].name;
const notifBody = `Created by ${games[randomItem].author}.`;
const notifImg = `data/img/${games[randomItem].slug}.jpg`;
const options = {
body: notifBody,
icon: notifImg,
};
swRegistration.showNotification(notifTitle, options);
setTimeout(randomNotification, 30000);
}
每 30 秒建立一個新的隨機通知,直到使用者覺得太煩人而停用它。(對於實際應用程式,通知的頻率應該低得多,而且更有用。)通知 API 的優勢在於它使用了作業系統的通知功能。這意味著即使在使用者未檢視 Web 應用程式時,也可以將通知顯示給使用者,並且通知看起來與原生應用程式顯示的通知類似。
推送 (Push)
推送比通知更復雜——我們需要訂閱一個伺服器,然後該伺服器會將資料傳送回應用程式。應用程式的服務工作執行緒將從推送伺服器接收資料,然後可以使用通知系統或其他所需機制來顯示這些資料。
這項技術仍處於非常初級的階段——一些工作示例使用了 Google Cloud Messaging 平臺,但正在重寫以支援 VAPID(自願應用程式標識),它為您的應用程式提供了額外的安全層。您可以檢視 Service Workers Cookbook 示例,嘗試使用 Firebase 設定推送訊息伺服器,或構建自己的伺服器(例如使用 Node.js)。
如前所述,要接收推送訊息,您必須有一個服務工作執行緒,其基礎知識已在 使用 Service Workers 讓 PWA 離線工作 文章中進行了介紹。在服務工作執行緒內部,透過呼叫 PushManager 介面的 getSubscription() 方法來建立推送服務訂閱機制。
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => registration.pushManager.getSubscription())
.then(/* … */);
一旦使用者訂閱成功,他們就可以從伺服器接收推送通知。
從伺服器端來看,整個過程出於安全原因必須使用公鑰和私鑰進行加密——允許任何人不安全地使用您的應用程式傳送推送訊息將是一個糟糕的主意。有關保護伺服器的詳細資訊,請參閱 Web Push 資料加密測試頁面。伺服器儲存使用者訂閱時接收到的所有資訊,以便以後需要時可以傳送訊息。
要接收推送訊息,我們可以在服務工作執行緒檔案中監聽 push 事件。
self.addEventListener("push", (e) => {
/* ... */
});
可以檢索資料,然後立即將其顯示為通知給使用者。例如,這可用於提醒使用者某事,或讓他們知道應用程式中提供的新內容。
推送示例
推送需要伺服器部分才能工作,因此我們無法將其包含在託管在 GitHub Pages 上的 js13kPWA 示例中,因為它僅提供靜態檔案託管。所有內容都在 Service Worker Cookbook 中進行了說明——請參閱 Push Payload 演示。
此演示由三個檔案組成:
index.js,其中包含我們的應用程式的原始碼。server.js,其中包含伺服器部分(使用 Node.js 編寫)。service-worker.js,其中包含服務工作執行緒特有的程式碼。
讓我們來探討一下所有這些內容。
index.js
index.js 檔案首先註冊服務工作執行緒。
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => registration.pushManager.getSubscription())
.then((subscription) => {
// subscription part
});
它比我們在 js13kPWA 演示 中看到的服務工作執行緒要複雜一些。在這種特定情況下,註冊後,我們使用註冊物件進行訂閱,然後使用生成的訂閱物件來完成整個過程。
在註冊部分,程式碼如下所示:
async (subscription) => {
if (subscription) {
return subscription;
}
};
如果使用者已訂閱,我們將返回訂閱物件並繼續到訂閱部分。如果未訂閱,我們將初始化一個新訂閱。
const response = await fetch("./vapidPublicKey");
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
應用程式獲取伺服器的公鑰並將響應轉換為文字;然後需要將其轉換為 Uint8Array(以支援 Chrome)。要了解有關 VAPID 金鑰的更多資訊,您可以閱讀 透過 Mozilla 的推送服務傳送 VAPID 標識的 WebPush 通知 部落格文章。
現在應用程式可以使用 PushManager 來訂閱新使用者。傳遞給 PushManager.subscribe() 方法的有兩個選項——第一個是 userVisibleOnly: true,這意味著傳送給使用者的所有通知都將對他們可見,第二個是 applicationServerKey,它包含我們已成功獲取並轉換的 VAPID 金鑰。
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey,
});
現在讓我們轉到訂閱部分——應用程式首先使用 Fetch 將訂閱詳細資訊作為 JSON 傳送到伺服器。
fetch("./register", {
method: "post",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({ subscription }),
});
然後定義“訂閱”按鈕的 onclick 函式:
document.getElementById("doIt").onclick = () => {
const payload = document.getElementById("notification-payload").value;
const delay = document.getElementById("notification-delay").value;
const ttl = document.getElementById("notification-ttl").value;
fetch("./sendNotification", {
method: "post",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({
subscription,
payload,
delay,
ttl,
}),
});
};
單擊按鈕時,fetch 會要求伺服器傳送帶有給定引數的通知:payload 是要在通知中顯示的文字,delay 定義通知將顯示前的延遲(以秒為單位),而 ttl 是生存時間設定,它允許通知在伺服器上保留指定的時間(也以秒為單位)。
現在,我們來看下一個 JavaScript 檔案。
server.js
伺服器部分是用 Node.js 編寫的,需要託管在合適的位置,這是一個單獨的文章主題。我們在此僅提供高層概述。
使用 web-push 模組 來設定 VAPID 金鑰,並在尚未提供金鑰時可選地生成它們。
const webPush = require("web-push");
if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
console.log(
"You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " +
"environment variables. You can use the following ones:",
);
console.log(webPush.generateVAPIDKeys());
return;
}
webPush.setVapidDetails(
"https://example.com",
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY,
);
接下來,一個模組定義並匯出了應用程式需要處理的所有路由:獲取 VAPID 公鑰、註冊以及傳送通知。您可以看到來自 index.js 檔案的變數:payload、delay 和 ttl。
module.exports = (app, route) => {
app.get(`${route}vapidPublicKey`, (req, res) => {
res.send(process.env.VAPID_PUBLIC_KEY);
});
app.post(`${route}register`, (req, res) => {
res.sendStatus(201);
});
app.post(`${route}sendNotification`, (req, res) => {
const subscription = req.body.subscription;
const payload = req.body.payload;
const options = {
TTL: req.body.ttl,
};
setTimeout(() => {
webPush
.sendNotification(subscription, payload, options)
.then(() => {
res.sendStatus(201);
})
.catch((error) => {
console.log(error);
res.sendStatus(500);
});
}, req.body.delay * 1000);
});
};
service-worker.js
我們最後要看的檔案是服務工作執行緒。
self.addEventListener("push", (event) => {
const payload = event.data?.text() ?? "no payload";
event.waitUntil(
self.registration.showNotification("ServiceWorker Cookbook", {
body: payload,
}),
);
});
它所做的就是新增一個 push 事件的監聽器,建立由從資料中獲取的文字組成的 payload 變數(或在資料為空時建立一個用於使用的字串),然後等待直到通知顯示給使用者。
如果您想了解更多關於示例的處理方式,請隨意探索 Service Worker Cookbook 中的其餘示例。那裡有大量工作示例,展示了一般用法,還有 Web 推送、快取策略、效能、離線工作等等。