在本程式碼研究室中,您將建構推播通知伺服器。伺服器會管理推送訂閱項目清單,並傳送通知給這些訂閱。
用戶端程式碼已完成。在本程式碼研究室中,您將使用伺服器端的功能。
重混範例應用程式,並在新分頁中開啟應用程式
系統會自動封鎖內嵌的 Glitch 應用程式通知,因此您無法在這個頁面上預覽應用程式。而是採取以下做法:
- 按一下「Remix to Edit」,讓專案可供編輯。
- 如要預覽網站,請按下「查看應用程式」,然後按下「全螢幕」圖示
。
執行中的應用程式會在新的 Chrome 分頁中開啟。在嵌入的 Glitch 中,按一下「View Source」,即可再次顯示程式碼。
執行本程式碼研究室時,請變更本頁內嵌 Glitch 中的程式碼。重新整理運作中的應用程式新分頁,查看變更。
熟悉入門應用程式及其程式碼
首先,請查看應用程式的用戶端 UI。
在新的 Chrome 分頁中:
按下「Control + Shift + J 鍵」(在 Mac 上則是「Command + Option + J 鍵」) 開啟開發人員工具。 再按一下「Console」(控制台) 分頁標籤即可。
嘗試點選 UI 中的按鈕 (請查看 Chrome 開發控制台取得輸出內容)。
註冊 Service Worker 會針對 Glitch 專案網址的範圍註冊 Service Worker。取消註冊 Service Worker 會移除 Service Worker。如果附加了推送訂閱項目,推送訂閱項目也會一併停用。
訂閱以推送會建立推送訂閱項目。只有在註冊 Service Worker,且用戶端程式碼中有
VAPID_PUBLIC_KEY
常數時才能使用此變數 (稍後將詳細說明),因此您還無法點選此常數。具備有效的推送訂閱項目時,「通知目前的訂閱」要求伺服器傳送通知至其端點。
通知所有訂閱項目,告訴伺服器傳送通知至資料庫中的所有訂閱端點。
請注意,部分端點可能處於閒置狀態。伺服器傳送通知給伺服器時,都有可能讓訂閱項目消失。
接著來看看伺服器端發生的情況。如要查看來自伺服器程式碼的訊息,請查看 Glitch 介面中的 Node.js 記錄。
在 Glitch 應用程式中,按一下「Tools」->「Log」。
系統可能會顯示類似「
Listening on port 3000
」的訊息,如果您嘗試在上線的應用程式 UI 中點選「通知目前的訂閱通知」或「通知所有訂閱內容」,您也會看到以下訊息:
TODO: Implement sendNotifications() Endpoints to send to: []
現在,讓我們看看一些程式碼。
public/index.js
包含已完成的用戶端程式碼。它會執行功能偵測、註冊和取消註冊 Service Worker,並控制使用者的推播通知訂閱。此外,也會將新增和已刪除的訂閱項目相關資訊傳送到伺服器。由於您的示範範圍只會使用伺服器功能,因此您不必編輯這個檔案 (只需填入
VAPID_PUBLIC_KEY
常數)。public/service-worker.js
是簡單的 Service Worker,可擷取推送事件並顯示通知。/views/index.html
包含應用程式 UI。.env
包含 Glitch 啟動時的環境變數,您將在.env
中填入傳送通知的驗證詳細資料。server.js
是您將在本程式碼研究室中處理大部分工作的檔案。起始程式碼會建立簡易的 Express 網路伺服器。您有四個 TODO 項目,在程式碼註解中會以
TODO:
標示。必要操作:在本程式碼研究室中,您將逐一瞭解這些 TODO 項目。
產生及載入 VAPID 詳細資料
您的第一個 TODO 項目是產生 VAPID 詳細資料、將這些詳細資料新增至 Node.js 環境變數,並根據新的值更新用戶端和伺服器程式碼。
背景
使用者訂閱通知時,必須信任應用程式及其伺服器的身分。使用者也必須確保,收到通知時,他們使用的是設定訂閱項目的同一個應用程式。並確保其他人無法讀取通知內容。
讓推播通知的安全性和私密性,這項通訊協定稱為「自主性應用程式伺服器識別 (VAPID)」,VAPID 使用公開金鑰密碼編譯技術驗證應用程式、伺服器和訂閱端點的身分,以及加密通知內容。
在這個應用程式中,您將使用 web-push npm 套件產生 VAPID 金鑰,以及加密和傳送通知。
導入作業
在這個步驟中,請為應用程式產生一對 VAPID 金鑰,並將這些金鑰新增至環境變數。在伺服器中載入環境變數,然後將公開金鑰新增為用戶端程式碼中的常數。
使用
web-push
程式庫的generateVAPIDKeys
函式建立一對 VAPID 金鑰。在 server.js 中,移除以下程式碼行中的註解:
server.js
// Generate VAPID keys (only do this once). /* * const vapidKeys = webpush.generateVAPIDKeys(); * console.log(vapidKeys); */ const vapidKeys = webpush.generateVAPIDKeys(); console.log(vapidKeys);
Glitch 重新啟動應用程式後,會將產生的金鑰輸出到 Glitch 介面中的 Node.js 記錄 (「不是」Chrome 控制台)。如要查看 VAPID 金鑰,請在 Glitch 介面中依序選取「Tools」->「Log」。
請務必從同一個金鑰組複製公開與私密金鑰!
Glitch 會每次編輯程式碼時重新啟動應用程式,因此產生的第一組按鍵可能會隨著輸出內容而無法顯示。
在 .env 中複製及貼上 VAPID 金鑰。以雙引號 (
"..."
) 括住鍵。在
VAPID_SUBJECT
中,您可以輸入"mailto:test@test.test"
。.env
# process.env.SECRET VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_SUBJECT= VAPID_PUBLIC_KEY="BN3tWzHp3L3rBh03lGLlLlsq..." VAPID_PRIVATE_KEY="I_lM7JMIXRhOk6HN..." VAPID_SUBJECT="mailto:test@test.test"
在 server.js 中,再次將這兩行程式碼註解排除,因為您只需要產生 VAPID 金鑰一次。
server.js
// Generate VAPID keys (only do this once). /* const vapidKeys = webpush.generateVAPIDKeys(); console.log(vapidKeys); */ const vapidKeys = webpush.generateVAPIDKeys(); console.log(vapidKeys);
在 server.js 中,從環境變數載入 VAPID 詳細資料。
server.js
const vapidDetails = { // TODO: Load VAPID details from environment variables. publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY, subject: process.env.VAPID_SUBJECT }
請一併複製公開金鑰並貼到用戶端程式碼中。
在 public/index.js 中為您複製到 .env 檔案的
VAPID_PUBLIC_KEY
輸入相同的值:public/index.js
// Copy from .env const VAPID_PUBLIC_KEY = ''; const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...'; ````
實作傳送通知的功能
背景
在這個應用程式中,您將使用 web-push npm 套件傳送通知。
這個套件會在呼叫 webpush.sendNotification()
時自動加密通知,因此不必擔心。
web-push 接受多個通知選項,例如您可以將標頭附加至郵件,並指定內容編碼。
在本程式碼研究室中,您只能使用兩個選項,這些選項定義如下:
let options = {
TTL: 10000; // Time-to-live. Notifications expire after this.
vapidDetails: vapidDetails; // VAPID keys from .env
};
TTL
(存留時間) 選項會設定通知的到期時間。這可讓伺服器避免在使用者不再需要時傳送通知給使用者。
vapidDetails
選項包含您從環境變數載入的 VAPID 金鑰。
導入作業
在 server.js 中,按照下列方式修改 sendNotifications
函式:
server.js
function sendNotifications(database, endpoints) {
// TODO: Implement functionality to send notifications.
console.log('TODO: Implement sendNotifications()');
console.log('Endpoints to send to: ', endpoints);
let notification = JSON.stringify(createNotification());
let options = {
TTL: 10000, // Time-to-live. Notifications expire after this.
vapidDetails: vapidDetails // VAPID keys from .env
};
endpoints.map(endpoint => {
let subscription = database[endpoint];
webpush.sendNotification(subscription, notification, options);
});
}
由於 webpush.sendNotification()
會傳回 promise,因此您可以輕鬆新增錯誤處理機制。
在 server.js 中,再次修改 sendNotifications
函式:
server.js
function sendNotifications(database, endpoints) {
let notification = JSON.stringify(createNotification());
let options = {
TTL: 10000; // Time-to-live. Notifications expire after this.
vapidDetails: vapidDetails; // VAPID keys from .env
};
endpoints.map(endpoint => {
let subscription = database[endpoint];
webpush.sendNotification(subscription, notification, options);
let id = endpoint.substr((endpoint.length - 8), endpoint.length);
webpush.sendNotification(subscription, notification, options)
.then(result => {
console.log(`Endpoint ID: ${id}`);
console.log(`Result: ${result.statusCode} `);
})
.catch(error => {
console.log(`Endpoint ID: ${id}`);
console.log(`Error: ${error.body} `);
});
});
}
處理新訂閱項目
背景
以下是使用者訂閱推播通知時會發生的情況:
使用者按一下「訂閱推送」。
用戶端會使用
VAPID_PUBLIC_KEY
常數 (伺服器的公開 VAPID 金鑰),產生伺服器專用的不重複subscription
物件。subscription
物件如下所示:{ "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...", "expirationTime": null, "keys": { "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...", "auth": "0IyyvUGNJ9RxJc83poo3bA" } }
用戶端將
POST
要求傳送至/add-subscription
網址,包括在內文中以字串化 JSON 格式訂閱的訂閱項目。伺服器從 POST 要求的內文擷取字串化的
subscription
,將字串剖析回 JSON,再將其新增至訂閱資料庫。資料庫會以自己的端點做為索引鍵,儲存訂閱項目:
{
"https://fcm...1234": {
endpoint: "https://fcm...1234",
expirationTime: ...,
keys: { ... }
},
"https://fcm...abcd": {
endpoint: "https://fcm...abcd",
expirationTime: ...,
keys: { ... }
},
"https://fcm...zxcv": {
endpoint: "https://fcm...zxcv",
expirationTime: ...,
keys: { ... }
},
}
現在,伺服器可以取得新的訂閱項目,並傳送通知。
導入作業
新訂閱的要求會傳送至 /add-subscription
路徑,也就是 POST 網址。您會在 server.js 中看到虛設常式路徑處理常式:
server.js
app.post('/add-subscription', (request, response) => {
// TODO: implement handler for /add-subscription
console.log('TODO: Implement handler for /add-subscription');
console.log('Request body: ', request.body);
response.sendStatus(200);
});
在實作中,這個處理常式必須:
- 從要求的主體擷取新訂閱。
- 存取有效訂閱項目的資料庫。
- 將新訂閱項目加入有效訂閱項目清單。
處理新訂閱項目的方式如下:
在 server.js 中,按照下列方式修改
/add-subscription
的路徑處理常式:server.js
app.post('/add-subscription', (request, response) => {
// TODO: implement handler for /add-subscription
console.log('TODO: Implement handler for /add-subscription');
console.log('Request body: ', request.body);
let subscriptions = Object.assign({}, request.session.subscriptions);
subscriptions[request.body.endpoint] = request.body;
request.session.subscriptions = subscriptions;
response.sendStatus(200);
});
處理取消訂閱作業
背景
伺服器不一定能知道訂閱項目何時失效,例如瀏覽器關閉 Service Worker 後,可能會清除訂閱項目。
不過,伺服器可以找出透過應用程式 UI 取消的訂閱項目。在這個步驟中,您將實作從資料庫移除訂閱項目的功能。
如此一來,伺服器就能避免向不存在的端點發出大量通知。這對於使用簡單的測試應用程式顯然並不重要,但對大規模來說至關重要。
導入作業
取消訂閱的要求會傳送至 /remove-subscription
POST 網址。
server.js 中的虛設常式路徑處理常式如下所示:
server.js
app.post('/remove-subscription', (request, response) => {
// TODO: implement handler for /remove-subscription
console.log('TODO: Implement handler for /remove-subscription');
console.log('Request body: ', request.body);
response.sendStatus(200);
});
在實作中,這個處理常式必須:
- 從要求的主體中擷取已取消訂閱項目的端點。
- 存取有效訂閱項目的資料庫。
- 從有效訂閱項目清單中移除已取消的訂閱項目。
來自用戶端的 POST 要求主體包含您需要移除的端點:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}
如何處理取消訂閱:
在 server.js 中,按照下列方式修改
/remove-subscription
的路徑處理常式:server.js
app.post('/remove-subscription', (request, response) => {
// TODO: implement handler for /remove-subscription
console.log('TODO: Implement handler for /remove-subscription');
console.log('Request body: ', request.body);
let subscriptions = Object.assign({}, request.session.subscriptions);
delete subscriptions[request.body.endpoint];
request.session.subscriptions = subscriptions;
response.sendStatus(200);
});