第一步是获得用户授权来向他们发送推送消息,然后我们就可以获得 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 和 Push 均受支持。下一步是“注册”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 目前仅支持对会生成用户可见消息的订阅使用 Push API。您可以通过调用 pushManager.subscribe({userVisibleOnly: true})
来指示这一点。如需了解详情,请访问 https://goo.gl/yqv4Q4。
目前,Chrome 一律不会实现一揽子静默推送。相反,规范作者正在探索预算 API 的概念,该 API 可根据 Web 应用的使用情况,允许 Web 应用发送一定数量的静默推送消息。
applicationServerKey 选项
我们在上一部分中简要提到了“应用服务器密钥”。推送服务使用“应用服务器密钥”来识别订阅用户的应用,并确保同一应用在向该用户发送消息。
应用服务器密钥是您的应用独有的公钥和私钥对。私钥应对您的应用保持保密状态,并且公钥可以自由共享。
传递到 subscribe()
调用中的 applicationServerKey
选项是应用的公钥。浏览器在订阅用户时会将其传递给推送服务,这意味着推送服务可以将应用的公钥与用户的 PushSubscription
相关联。
下图说明了这些步骤。
- 您的 Web 应用在浏览器中加载,您调用
subscribe()
,并传入应用服务器公钥。 - 然后,浏览器向推送服务发出网络请求,该服务将生成端点,将此端点与应用公钥相关联,然后将该端点返回给浏览器。
- 浏览器会将此端点添加到通过
subscribe()
promise 返回的PushSubscription
中。
如果您稍后想要发送推送消息,则需要创建 Authorization 标头,其中包含使用应用服务器的私钥签名的信息。当推送服务收到发送推送消息的请求时,它可以查询与接收请求的端点相关联的公钥,从而验证已签名的 Authorization 标头。如果签名有效,推送服务知道它必须来自具有匹配私钥的应用服务器。从本质上讲,它是一项安全措施,可防止任何其他人向应用的用户发送消息。
从技术上讲,applicationServerKey
是可选的。不过,Chrome 上最简单的实现需要使用它,将来其他浏览器可能会用到它。在 Firefox 中是可选操作。
定义应用服务器密钥的规范是 VAPID 规范。当您读到引用“应用服务器密钥”或“VAPID 密钥”的内容时,请记住它们是同一回事。
如何创建应用服务器密钥
您可以通过访问 web-push-codelab.glitch.me 来创建一组应用服务器公钥和私钥,也可以使用 web-push 命令行通过执行以下操作来生成密钥:
$ npm install -g web-push
$ web-push generate-vapid-keys
您只需为应用创建一次这些密钥,只需确保私钥保持私钥即可。(是的,我刚说过。)
权限和 subscription()
调用 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
对象包含用于加密通过推送消息发送的消息数据的值(我们将在本部分的后面部分讨论)。
定期重新订阅以防过期
订阅推送通知时,您通常会收到 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);
});
}
向您的服务器发送订阅
拥有推送订阅后,您需要将其发送到您的服务器。您可以自行决定如何执行此操作,但有一点需要注意,那就是使用 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.');
}
});
}
节点服务器收到此请求,并将数据保存到数据库中以供稍后使用。
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 推送协议,描述了您的应用需要发出的网络请求才能触发推送消息。
如果我通过桌面设备订阅了某个用户,该用户是否也会在手机上进行订阅?
很遗憾,不会。用户必须在其希望用来接收消息的每个浏览器上注册推送。另外值得注意的是,这将需要用户在每台设备上授予权限。