在此 Codelab 中,您将构建一个推送通知服务器。服务器将管理推送订阅的列表并向其发送通知。
客户端代码已经完成 - 在此 Codelab 中,您将学习服务器端功能。
混剪示例应用,并在新标签页中查看该应用
系统会自动屏蔽嵌入式 Glitch 应用的通知,因此您无法在此页面上预览该应用。而应执行以下操作:
- 点击 Remix to Edit 使项目可修改。
- 如需预览网站,请按查看应用,然后按全屏
。
已发布的应用会在新的 Chrome 标签页中打开。在嵌入的 Glitch 中,点击 View Source 可再次显示代码。
在学习此 Codelab 时,请更改本页面上嵌入式 Glitch 中的代码。使用您的实时应用刷新新标签页以查看更改。
熟悉起始应用及其代码
首先查看应用的客户端界面。
在新的 Chrome 标签页中:
按 `Control+Shift+J`(在 Mac 上,则按 `Command+Option+J`)打开开发者工具。 点击控制台标签页。
尝试点击界面中的按钮(在 Chrome 开发者控制台中查看输出)。
注册 Service Worker:为 Glitch 项目网址作用域注册 Service Worker。取消注册 Service Worker 会移除 Service Worker。如果与其关联了推送订阅,则推送订阅也将停用。
“订阅推送”会创建推送订阅。仅当 Service Worker 已注册且客户端代码中存在
VAPID_PUBLIC_KEY
常量时,它才可用(稍后会详细介绍),因此目前您无法点击它。如果您有处于有效状态的推送订阅,通知当前订阅会请求服务器向其端点发送通知。
通知所有订阅告知服务器向其数据库中的所有订阅端点发送通知。
请注意,其中一些端点可能处于非活跃状态。订阅在服务器向其发送通知时可能会消失。
我们来看一下服务器端会发生什么情况。要查看来自服务器代码的消息,请查看 Glitch 界面内的 Node.js 日志。
在 Glitch 应用中,依次点击 Tools -> Logs。
您可能会看到类似
Listening on port 3000
的消息。如果您尝试在实时应用界面中点击通知当前订阅或通知所有订阅,您还会看到以下消息:
TODO: Implement sendNotifications() Endpoints to send to: []
现在,我们来看一些代码。
public/index.js
包含完成后的客户端代码。它用于执行功能检测、注册和取消注册 Service Worker,并控制用户的推送通知订阅。它还会向服务器发送有关新订阅和已删除订阅的信息。由于您只想处理服务器功能,因此无需修改此文件(除了填充
VAPID_PUBLIC_KEY
常量之外)。public/service-worker.js
是一个简单的 Service Worker,可捕获推送事件并显示通知。/views/index.html
包含应用界面。.env
包含 Glitch 启动时在应用服务器中加载的环境变量。您将在.env
中填充用于发送通知的身份验证详细信息。server.js
是您将在此 Codelab 中完成大部分工作的文件。起始代码会创建一个简单的 Express Web 服务器。您有四项 TODO 项,在代码注释中使用
TODO:
进行了标记。您需要执行的操作:在此 Codelab 中,您将逐一完成这些 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 界面(而非 Chrome 控制台)内的 Node.js 日志中。如需查看 VAPID 密钥,请在 Glitch 界面中依次选择 Tools -> Logs。
请务必从同一密钥对中复制公钥和私钥!
每次您修改代码时,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 密钥也复制并粘贴到客户端代码中。
在 public/index.js 中,为
VAPID_PUBLIC_KEY
输入复制到 .env 文件的相同值:public/index.js
// Copy from .env const VAPID_PUBLIC_KEY = ''; const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...'; ````
实现发送通知的功能
背景
在此应用中,您将使用 web-push npm 软件包发送通知。
此软件包会在调用 webpush.sendNotification()
时自动加密通知,因此您无需担心。
web-push 接受多个通知选项,例如,您可以在邮件中附加标头并指定内容编码。
在此 Codelab 中,您只需使用下面这行代码定义的两个选项:
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" } }
客户端向
/add-subscription
网址发送POST
请求,并在正文中以字符串化 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 时,订阅可能会被擦除。
但是,服务器可以通过应用界面发现被取消的订阅。在此步骤中,您将实现从数据库中移除订阅的功能。
这样,服务器就可以避免向不存在的端点发送大量通知。显然,对于简单的测试应用而言,这并不重要,但在应用更大范围内,这一点就变得非常重要。
实现
取消订阅的请求会发送到 /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);
});