如需发送推送消息,您必须先获得用户的许可,然后让用户的设备订阅推送服务。这涉及使用 JavaScript API 获取 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 时,您需要告知浏览器 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 会解析。如果发生错误,promise 会拒绝。
注意:如果 register() 拒绝,请在 Chrome 开发者工具中检查 JavaScript 是否存在拼写错误或错误。
register() 解析后,会返回 ServiceWorkerRegistration。您可以使用此注册来访问 PushManager API。
PushManager API 浏览器兼容性
请求权限
注册 Service Worker 并获得权限后,请向用户请求发送推送消息的权限。
用于获取权限的 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 会解析;否则,它会抛出错误,并且 promise 会拒绝。
处理用户点击屏蔽按钮的极端情况。如果发生这种情况,您的 Web 应用将无法再次向用户请求权限。用户必须在设置面板中更改应用的权限状态,才能手动解除对应用的屏蔽。请仔细考虑何时以及如何请求权限,因为如果用户点击了屏蔽,则很难撤销该决定。
如果用户了解应用请求权限的原因,大多数用户都会授予权限。
本文档稍后会讨论一些热门网站如何请求权限。
使用 PushManager 订阅用户
注册 Service Worker 并获得权限后,您可以通过调用 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 值是与浏览器达成的象征性协议,即 Web 应用每次收到推送消息时都会显示通知(即没有静默推送)。
您必须传递 true 值。如果您未添加 userVisibleOnly 键或传递 false,则会收到以下错误:
Chrome currently only supports the Push API for subscriptions that will result
in user-visible messages. You can indicate this by calling
`pushManager.subscribe({userVisibleOnly: true})` instead. See
[https://goo.gl/yqv4Q4](https://goo.gl/yqv4Q4) for more details.
Chrome 仅支持会产生用户可见消息的订阅。通过调用 pushManager.subscribe({userVisibleOnly: true}) 来指明这一点。如需了解详情,请参阅 https://goo.gl/yqv4Q4。
Chrome 似乎不会实现全面静默推送。相反,规范作者正在探索一种预算 API,该 API 允许 Web 应用根据 Web 应用的使用情况发送一定数量的静默推送消息。
applicationServerKey 选项
本文档之前提到过应用服务器密钥。推送服务使用应用服务器密钥来识别订阅用户的应用,并确保同一应用向该用户发送消息。
应用服务器密钥是应用特有的公钥和私钥对。请对应用的私钥保密,并随意分享公钥。
传入 subscribe() 调用的 applicationServerKey 选项是应用的公钥。浏览器在为用户订阅时会将此密钥传递给推送服务,从而使推送服务能够将应用的公钥与用户的 PushSubscription 相关联。
下图说明了这些步骤。
- 在浏览器中加载 Web 应用,然后调用
subscribe(),并传递您的公共应用服务器密钥。 - 然后,浏览器向推送服务发出网络请求,推送服务会生成一个端点,将此端点与应用的公钥相关联,并将该端点返回给浏览器。
- 浏览器会将此端点添加到
PushSubscription中,该端点由subscribe()promise 返回。
发送推送消息时,请创建一个 Authorization 标头,其中包含使用应用服务器的私钥签名的信息。当推送服务收到发送推送消息的请求时,它会通过查找与接收请求的端点关联的公钥来验证此已签名的 Authorization 标头。如果签名有效,推送服务就会知道请求来自具有匹配私钥的应用服务器。这是一项安全措施,可防止他人向您应用的用户发送消息。
严格意义上来说,applicationServerKey 是可选的。不过,Chrome 上最简单的实现方式需要此权限,其他浏览器将来可能也需要此权限。在 Firefox 上,此步骤是可选的。
VAPID 规范定义了应用服务器密钥。当您看到对应用服务器密钥或 VAPID 密钥的引用时,请记住它们是相同的。
创建应用服务器密钥
您可以使用 web-push 命令行生成密钥,从而创建一组公钥和私钥形式的应用服务器密钥,如下所示:
$ npm install -g web-push
$ web-push generate-vapid-keys
只需为您的应用创建一次这些密钥,并确保私钥不外泄。
权限和 subscribe()
调用 subscribe() 会产生一个副作用。如果您的 Web 应用在您调用 subscribe() 时没有显示通知的权限,浏览器会为您请求这些权限。如果您的界面支持此流程,则此方法非常有用;但如果您希望获得更多控制权(大多数开发者都希望如此),请使用本文档前面讨论的 Notification.requestPermission() API。
PushSubscription 概览
您调用 subscribe(),传递选项,并收到一个解析为 PushSubscription 的 promise。例如:
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 对象包含用于对随推送消息发送的消息数据进行加密的值。(本文档稍后会讨论消息加密。)
向服务器发送订阅
获得推送订阅后,请将其发送到您的服务器。您可以自行决定如何发送,但建议您使用 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.');
}
});
}
Node.js 服务器收到此请求,并将数据保存到数据库中以供日后使用。
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 称为 Web 推送协议,它描述了应用为触发推送消息而发出的网络请求。
如果用户在桌面设备上订阅了,那么他们在手机上也会订阅吗?
不能。用户必须在每个要接收消息的浏览器上注册推送消息。这还需要用户在每台设备上授予权限。
后续步骤
- Web 推送通知概览
- 推送消息的工作原理
- 为用户订阅
- 权限用户体验
- 使用 Web 推送库发送消息
- Web 推送协议
- 处理推送事件
- 显示通知
- 通知行为
- 常见通知模式
- 推送通知常见问题解答
- 常见问题和报告 bug