使用 Web Push 时的痛点之一是触发推送消息非常“棘手”。要触发推送消息,应用程序需要向推送服务发出 POST 请求,遵循 web push 协议。要在所有浏览器中使用推送,您需要使用 VAPID(也称为应用程序服务器密钥),这基本上需要设置一个标头,其值证明您的应用程序可以向用户发送消息。要使用推送消息发送数据,数据需要 加密,并且需要添加特定的标头,以便浏览器可以正确解密消息。
触发推送的主要问题是,如果您遇到问题,则很难诊断问题。随着时间的推移和更广泛的浏览器支持,这种情况正在改善,但仍然远非易事。因此,我强烈建议使用库来处理推送消息的加密、格式化和触发。
如果您真的想了解库正在做什么,我们将在下一节中介绍它。现在,我们将了解如何管理订阅以及使用现有的 Web Push 库来发出推送请求。
在本节中,我们将使用 web-push Node 库。其他语言会有所不同,但不会太相似。我们正在研究 Node,因为它是 JavaScript,对于读者来说应该最容易理解。
我们将完成以下步骤
- 将订阅发送到我们的后端并保存它。
- 检索已保存的订阅并触发推送消息。
保存订阅
从数据库保存和查询 PushSubscription
会因您的服务器端语言和数据库选择而异,但了解如何完成它的示例可能会很有用。
在演示网页中,PushSubscription
通过发出简单的 POST 请求发送到我们的后端
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.');
}
});
}
我们演示中的 Express 服务器有一个匹配的请求侦听器,用于 /api/save-subscription/
端点
app.post('/api/save-subscription/', function (req, res) {
在此路由中,我们验证订阅只是为了确保请求没问题,而不是充满了垃圾
const isValidSaveRequest = (req, res) => {
// Check the request body has at least an endpoint.
if (!req.body || !req.body.endpoint) {
// Not a valid subscription.
res.status(400);
res.setHeader('Content-Type', 'application/json');
res.send(
JSON.stringify({
error: {
id: 'no-endpoint',
message: 'Subscription must have an endpoint.',
},
}),
);
return false;
}
return true;
};
如果订阅有效,我们需要保存它并返回适当的 JSON 响应
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.',
},
}),
);
});
此演示使用 nedb 来存储订阅,它是一个简单的基于文件的数据库,但您可以使用您选择的任何数据库。我们仅使用它是因为它不需要任何设置。对于生产环境,您需要使用更可靠的东西。(我倾向于坚持使用旧的 MySQL。)
function saveSubscriptionToDatabase(subscription) {
return new Promise(function (resolve, reject) {
db.insert(subscription, function (err, newDoc) {
if (err) {
reject(err);
return;
}
resolve(newDoc._id);
});
});
}
发送推送消息
在发送推送消息时,我们最终需要一些事件来触发向用户发送消息的过程。一种常见的方法是创建一个管理页面,让您可以配置和触发推送消息。但是您可以创建一个程序在本地运行,或任何其他允许访问 PushSubscription
列表并运行代码来触发推送消息的方法。
我们的演示有一个“类似管理”的页面,可让您触发推送。由于它只是一个演示,因此它是公共页面。
我将逐步介绍使演示工作所需的每个步骤。这些将是简单的步骤,以便每个人都可以跟上,包括 Node 新手。
当我们讨论订阅用户时,我们介绍了将 applicationServerKey
添加到 subscribe()
选项。在后端,我们将需要此私钥。
在演示中,这些值像这样添加到我们的 Node 应用程序中(我知道代码很枯燥,但只是想让您知道没有魔法)
const vapidKeys = {
publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};
接下来,我们需要为我们的 Node 服务器安装 web-push
模块
npm install web-push --save
然后,在我们的 Node 脚本中,我们像这样要求 web-push
模块
const webpush = require('web-push');
现在我们可以开始使用 web-push
模块了。首先,我们需要告诉 web-push
模块关于我们的应用程序服务器密钥。(请记住,它们也称为 VAPID 密钥,因为那是规范的名称。)
const vapidKeys = {
publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};
webpush.setVapidDetails(
'mailto:web-push-book@gauntface.com',
vapidKeys.publicKey,
vapidKeys.privateKey,
);
请注意,我们还包含了一个“mailto:”字符串。此字符串需要是 URL 或 mailto 电子邮件地址。此信息实际上将作为触发推送请求的一部分发送到 Web Push 服务。这样做是为了如果 Web Push 服务需要与发送者联系,他们有一些信息可以让他们这样做。
这样,web-push
模块就可以使用了,下一步是触发推送消息。
演示使用假装的管理面板来触发推送消息。

单击“触发推送消息”按钮将向 /api/trigger-push-msg/
发出 POST 请求,这是我们的后端发送推送消息的信号,因此我们在此端点的 express 中创建路由
app.post('/api/trigger-push-msg/', function (req, res) {
收到此请求后,我们从数据库中获取订阅,并为每个订阅触发推送消息。
return getSubscriptionsFromDatabase().then(function (subscriptions) {
let promiseChain = Promise.resolve();
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i];
promiseChain = promiseChain.then(() => {
return triggerPushMsg(subscription, dataToSend);
});
}
return promiseChain;
});
然后,函数 triggerPushMsg()
可以使用 web-push 库向提供的订阅发送消息。
const triggerPushMsg = function (subscription, dataToSend) {
return webpush.sendNotification(subscription, dataToSend).catch((err) => {
if (err.statusCode === 404 || err.statusCode === 410) {
console.log('Subscription has expired or is no longer valid: ', err);
return deleteSubscriptionFromDatabase(subscription._id);
} else {
throw err;
}
});
};
对 webpush.sendNotification()
的调用将返回一个 promise。如果消息发送成功,promise 将 resolve,我们无需执行任何操作。如果 promise 拒绝,您需要检查错误,因为它会告诉您 PushSubscription
是否仍然有效。
要确定来自推送服务的错误类型,最好查看状态代码。错误消息在推送服务之间有所不同,有些比其他服务更有帮助。
在此示例中,它检查状态代码 404
和 410
,它们是“未找到”和“已删除”的 HTTP 状态代码。如果我们收到其中之一,则表示订阅已过期或不再有效。在这些情况下,我们需要从数据库中删除订阅。
如果出现其他错误,我们只需 throw err
,这将使 triggerPushMsg()
返回的 promise 拒绝。
当我们更详细地查看 Web Push 协议时,我们将在下一节中介绍其他一些状态代码。
循环遍历订阅后,我们需要返回 JSON 响应。
.then(() => {
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-send-messages',
message: `We were unable to send messages to all subscriptions : ` +
`'${err.message}'`
}
}));
});
我们已经介绍了主要的实现步骤
- 创建一个 API,将订阅从我们的网页发送到我们的后端,以便它可以将它们保存到数据库中。
- 创建一个 API 来触发推送消息的发送(在本例中,是从假装的管理面板调用的 API)。
- 从我们的后端检索所有订阅,并使用 web-push 库之一向每个订阅发送消息。
无论您的后端是什么(Node、PHP、Python 等),实现推送的步骤都将是相同的。
接下来,这些 web-push 库究竟在为我们做什么?
下一步去哪里
- Web Push 通知概述
- 推送的工作原理
- 订阅用户
- 权限用户体验
- 使用 Web Push 库发送消息
- Web Push 协议
- 处理推送事件
- 显示通知
- 通知行为
- 常见通知模式
- Push 通知常见问题解答
- 常见问题和报告错误