原文地址:Subscribing a Userhtml
译文地址:订阅一个用户git
译者:刘鹏github
第一步是从用户那里获取发送消息的权限,而后才能着手于 PushSubscription
。数据库
实现这一步的 Javascript API 是至关直接,因此让咱们来一步一步看一下这个逻辑流程。npm
首先,咱们须要检查用户当前的浏览器是否支持推送消息。能够经过下面两个简单的方法来检测。json
if (!('serviceWorker' in navigator)) {
// 此浏览器不支持 Service Worker,禁用或隐藏 UI
return;
}
if (!('PushManager' in window)) {
// 此浏览器不支持推送,禁用或隐藏 UI
return;
}
复制代码
虽然越来越多的浏览器对 service worker 和推送消息进行了支持,但对这二者同时进行特征检测而且进行渐进加强老是一个好主意。api
经过特征检测,咱们已经知道 service worker 和推送二者都已经支持了。下一步就是去注册咱们的 service worker。promise
当注册 service worker 的时候,至关于告诉浏览器咱们的 service worker 文件在哪里。这个文件依然是 JavaScript,可是浏览器会给它访问系统 service worker API 的权限,包括推送。 更确切的说,浏览器是在 service worker 环境运行这个文件。浏览器
经过调用 navigator.serviceWorker.register()
便可注册一个 service worker,同时将咱们文件的路径做为参数传入。以下面所示:
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()
以后,后台浏览器会进行下面几个步骤:
register()
以后返回的 promise 对象将会调用 resolve 方法。若是有任何错误发生,promise 对象会调用 reject 方法。若是
register()
reject 了,请在 Chrome 的开发者工具当中再检查一遍你的 JavaScript 代码中的拼写错误或逻辑错误。
若是 register()
确实 resolve 了, 它会返回一个 ServiceWorkerRegistration 的方法。咱们将使用它来访问推送管理 API。
咱们注册了 service worker,为订阅用户作好了准备,下一步就是从用户那里获取给他们发送消息的权限。
获取权限的 API 相对简单,可是不太好的是这个 API 最近由原来的回调方式变为返回一个 Promise 对象。这个变更形成了咱们不能分辨当前浏览器究竟实现了哪个,因此必须同时实现并处理二者。
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 对象会 resolve,不然的话咱们会抛出一个错误让 promise 对象拒绝。
还有一个边界状况咱们必定要处理,那就是用户是否点击了 Block 的按钮。若是这个发生了,咱们将不再可以跟用户请求受权。他们必须手动地 unblock 咱们的应用,改变咱们应用的权限状态,这个入口隐藏在浏览器的设置面板。 你须要仔细想一想以什么方法以及在什么时间向用户询问受权,由于若是他们点击 block,再想让他们更改这个决定并非那么容易。
好消息是,只要用户知道为何须要这个权限,大多数用户都很乐意受权给咱们。
后续,咱们将看看一些流行的站点是怎么请求受权的。
一旦咱们注册了 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 选项,而且和浏览器达成了一个象征性的协议,给此选项传入一个 true 值,这样的话每次收到一个推送,Web 应用都会展现出一个通知 (也就是说没有静默推送)。
因此说当前,你必须传入一个为 true 的值。若是你没有传入一个 userVisibleOnly 的键值或者传入的是 false 值,你会获得以下的错误:
Chrome 当前仅支持可以产生让用户可见消息的推送 API 的订阅。你能够调用
pushManager.subscribe({useVisibleOnly: true})
。查看 goo.gl/yqv4Q4 获取更多详情。
当前看起来,在 Chrome 当中,彻底的静默推送永远不会实现。规范编写者正在探索一个预算 API 的概念,它会基于用户对 Web App 的使用而给开发者们必定量的静默推送消息次数。
在以前的章节当中,咱们提到了 application server keys,推送服务使用它来鉴别订阅用户的服务应用,而且确保是一样的应用发送消息给那个订阅用户。
Application server keys 是一对公私钥的键值对,对于你的应用来讲是独一无二的。私钥应该对你的应用保密,而公钥能够自由地分享。
传入到 subscribe()
方法的 applicationServerKey 选项应该是公钥。当订阅一个用户的时候,浏览器会将这个值传递给推送服务,这意味着推送服务能够将你应用的公钥和用户的 PushSubscription
绑定起来。
下面的图描述了这些步骤:
subscribe()
,传入你的 application server key 中的公钥。subscribe()
返回的 Promise 对象 PushSubscription 当中。当你后续想要发送一个推送消息,须要建立一个 Authorization 的 header 头,这个 header 头将包含使用应用服务器的私钥签名以后的信息。 当推送服务接收到一个要求发送推送消息的请求,它经过查询关联到该请求的 endpoint 值的公钥,来验证该请求中签名过的 Authorization 的 header 头。若是签名是合法的,推送服务知道它必定来自于拥有匹配的私钥的应用服务器。总的来讲,它是用来防止其余人伪造身份发送消息给应用用户的一个安全措施。
从技术上来讲,applicationSecretKey
是一个可选项。然而,在 Chrome 浏览器上最容易的实现方案是须要它的,其余浏览器在之后也可能须要它。在 Firefox 中当前是可选项。
在 VAPID spec 中定义了 application server key 的规范。记住 application server key 和 VAPID keys 是同一个概念。
你能够经过访问 web-push-codelab.glitch.me 建立 application server keys 的公私钥。或者也可使用 web-push command line 经过下面几步来生成密钥。
$ npm install -g web-push
$ web-push generate-vapid-keys
复制代码
你只须要为你的应用生成一次密钥,而且确保你把私钥保管好(是的,我刚才提到过)。
在调用 subscribe()
时有一个反作用。就是你在调用它的时候,若是 Web 应用没有得到弹出通知的许可,浏览器会为你请求许可。 若是你的 UI 和这个流程是匹配的,这会颇有用。可是若是你须要更多的控制(我认为绝大多数开发者都是这样想的),请使用咱们以前用过的 Notification.requestPermission()
。
PushSubscription
咱们调用 subscribe()
,传入一些选项,而后得到一个 promise 对象,这个对象 resolve 返回的就是 PushSubscription
。相应的代码以下:
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
值就是推送服务的 URL。若是要触发一个推送消息的话,能够向这个 URL 发送一个 POST 请求。
keys
对象用于加密推送消息数据。
一旦有了一个推送订阅,你想要把它发送到你的服务器。怎么来作彻底由你,可是一个小提示就是使用 JSON.stringify()
来从订阅对象当中获取全部的必需数据。 固然,你也能够手动拼凑成相同的结果:
const subscriptionObject = {
endpoint: pushSubscription.endpoint,
keys: {
p256dh: pushSubscription.getKeys('p256dh'),
auth: pushSubscription.getKeys('auth')
}
};
// 上面和下面的输出是同样的
const subscriptionObjectToo = JSON.stringify(pushSubscription);
复制代码
在 Web 页面当中,像下面同样完成一个订阅的发送:
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 服务接收到这个请求以后,保存数据到数据库当中供之后使用。
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
的详细信息,就能够在任何想要的时候给用户发送一条消息了。
当前人们常常问的问题以下:
我能够更换浏览器使用的推送服务吗?
不行。推送服务是由浏览器选择的。正如咱们看到的,当咱们调用 subscribe()
时,浏览器会产生一个发送给推送服务的网络请求,来获取组成 PushSubscription 的细节信息。
每一个浏览器都使用不一样的推送服务,那它们会有不一样的 API 吗?
全部的推送服务拥有相同的 API。
这个相同的 API 被称为 Web 推送协议,它描述了你的应用须要怎样的网络请求来触发一个推送消息。
若是用户在桌面版进行了订阅,那他们是否是同时在他们的手机版也订阅了?
不幸的是,并无。一个用户必须在他想要接收消息的全部浏览器都进行注册推送。值得注意的是,用户也须要在每个设备上都进行受权。