最终目的是为了实现消息推送到用户设备并可以显示更新通知。
实施推送的三个关键步骤是:
- 添加客户端逻辑以订阅用户推送(即Web应用程序中注册用户以推送消息的JavaScript和UI)。
- 来自后端/应用程序的API调用,触发推送消息到用户的设备。
- 服务工作者JavaScript文件,当推送到达设备时将收到“推送事件”。在这个JavaScript中,您将能够显示通知。
第一步是“订阅”用户推送消息。
订阅用户需要两件事。
- 首先,获得用户的许可以向他们发送推送消息。
- 第二,然后PushSubscription从浏览器中获取。
PushSubscription包含向该用户发送推送消息所需的所有信息。您可以“将”视为该用户设备的ID。
特征检测
首先,我们需要检查当前浏览器是否实际支持推送消息。我们可以通过两个简单的检查来检查是否支持推送。
- 在导航器上检查serviceWorker。
- 检查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;
}
注册服务工作者
当我们注册服务工作者时,我们告诉浏览器我们的服务工作者文件在哪里。该文件仍然只是JavaScript,但浏览器将“授予它访问”服务工作者API,包括推送。
更确切地说,浏览器在服务工作者环境中运行该文件。
要注册服务工作者,请调用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);
});
}
- 下载服务工作文件。
- 运行JavaScript。
- 如果一切正常并且没有错误,则返回的承诺register() 将解决。如果有任何类型的错误,承诺将拒绝。
register()
时候,它返回一个ServiceWorkerRegistration
。我们将使用此来访问PushManager API。
请求许可
我们已经注册了我们的服务工作者并准备订阅用户,下一步是获得用户的许可以向他们发送推送消息。
获取权限的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, denied, or default.)
在上面的示例代码中,askPermission()如果授予了权限,则通过解析返回的promise ,否则我们会抛出一个错误,使得promise被拒绝。
您需要处理的一个边缘情况是用户单击“阻止”按钮。如果发生这种情况,您的网络应用将无法再次要求用户获得许可。他们必须通过更改其权限状态来手动“取消阻止”您的应用,该权限状态隐藏在设置面板中。仔细考虑如何以及何时向用户请求许可,因为如果他们点击阻止,则不是一种简单的方法来反转该决定。
好消息是,大多数用户都乐于给予许可,只要他们知道为什么要求许可。
使用PushManager订阅用户
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;
});
}
- 您的Web应用程序已加载到浏览器中,您可以调用subscribe(),传入公共应用程序服务器密钥。
- 然后,浏览器向推送服务发出网络请求,推送服务将生成端点,将此端点与应用程序公钥相关联,并将端点返回到浏览器。
- 浏览器会将此端点添加到PushSubscription,通过subscribe()promise 返回的 端点。
在调用subscribe()方法时,我们传入一个options对象,它包含必需参数和可选参数。
当您以后想要发送推送消息时,您需要创建一个Authorization标头,其中包含使用您的应用程序服务器的私钥签名的信息。当推送服务接收到发送推送消息的请求时,它可以通过查找链接到接收请求的端点的公钥来验证该签名的授权报头。如果签名有效,则推送服务知道它必须来自具有匹配私钥的应用服务器 。它基本上是一种安全措施,可以防止其他人向应用程序的用户发送消息。
PushSubscription对象包含向该用户发送推送消息所需的所有必需信息
{
"endpoint": "https://some.pushservice.com/something-unique",
"keys": {
"p256dh":
"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
"auth":"FPssNDTKnInHVndSTdbKFw=="
}
}
这endpoint是推送服务URL。要触发推送消息,请对此URL发出POST请求。
该keys对象包含用于加密通过推送消息发送的消息数据的值
userVisibleOnly选项
当推送首次添加到浏览器时,开发人员是否应该能够发送推送消息而不显示通知存在不确定性。这通常被称为静默推送,因为用户不知道在后台发生了某些事情。
令人担忧的是,开发人员可能会做一些讨厌的事情,例如在用户不知情的情况下持续跟踪用户的位置。
为了避免这种情况并让规范作者有时间考虑如何最好地支持此功能,userVisibleOnly添加了该选项并且传入值true是与浏览器的符号协议,每次收到推送时Web应用程序都会显示通知(即没有沉默的推动)。
目前你必须传入一个值true。如果您不包含 userVisibleOnly密钥或传入,false您将收到以下错误:
Chrome目前仅支持用于订阅的Push API,这将导致用户可见的消息。你可以通过调用
pushManager.subscribe({userVisibleOnly: true})
来表明这一点 。有关详细信息,请参阅https://goo.gl/yqv4Q4。
applicationServerKey选项
推送服务使用“应用服务器密钥”来标识订阅用户的应用程序,并确保相同的应用程序正在向该用户发送消息。
应用程序服务器密钥是公钥和私钥对,对您的应用程序而言是唯一的。私钥应该对您的应用程序保密,公钥可以自由共享。
applicationServerKey传递给subscribe()调用的选项是应用程序的公钥。在订阅用户时,浏览器将此传递到推送服务,这意味着推送服务可以将应用程序的公钥绑定到用户的PushSubscription。
您可以通过访问web-push-codelab.glitch.me来创建公共和私有应用程序服务器密钥集, 也可以 通过执行以下操作使用web-push命令行生成密钥
$ npm install -g web-push
$ web-push generate-vapid-keys
将订阅发送到您的服务器
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.'
}
}));
});
});
使用Web推送库发送消息
保存订阅
使用网络推送时的一个难点是触发推送消息非常“繁琐”。要触发推送消息,应用程序需要按照Web推送协议向推送服务发出POST请求。要在所有浏览器中使用push,您需要使用VAPID (也称为应用程序服务器密钥),这基本上需要设置一个标头,其值证明您的应用程序可以向用户发送消息。要使用推送消息发送数据,需要对数据进行 加密并添加特定标头,以便浏览器可以正确解密消息。
触发推送的主要问题是,如果遇到问题,很难诊断问题。随着时间的推移和浏览器的广泛支持,这种情况正在改善,但这并不容易。
我们将使用web-push节点库。
这个演示使用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);
});
});
};
发送推送消息
在发送推送消息时,我们最终需要一些事件来触发向用户发送消息的过程。一种常见的方法是创建一个管理页面,让您配置并触发推送消息。但是您可以创建一个在本地运行的程序或任何其他允许访问PushSubscriptions代码列表并运行代码以触发推送消息的方法。
接下来我们需要web-push为我们的Node服务器安装模块:
npm install web-push --save
然后在我们的Node脚本中,我们需要在web-push模块中这样:
const webpush = require('web-push');
首先,我们需要告诉web-push模块我们的应用服务器密钥
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推送服务。这样做的原因是,如果网络推送服务需要与发送者联系,他们会有一些信息可以让他们这样做。
有了这个,web-push模块就可以使用了,下一步是触发推送消息。
该演示使用假装管理面板来触发推送消息。
单击“触发推送消息”按钮将发出POST请求,/api/trigger-push-msg/ 这是我们后端发送推送消息的信号,因此我们为此端点创建快速路由:
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推送库向提供的订阅发送消息。
const triggerPushMsg = function(subscription, dataToSend) {
return webpush.sendNotification(subscription, dataToSend)
.catch((err) => {
if (err.statusCode === 410) {
return deleteSubscriptionFromDatabase(subscription._id);
} else {
console.log('Subscription is no longer valid: ', err);
}
});
};
webpush.sendNotification()将返回一个承诺。如果消息已成功发送,则承诺将解决,我们无需执行任何操作。如果承诺拒绝,您需要检查错误,因为它会告知您PushSubscription是否仍然有效。
要确定推送服务的错误类型,最好查看状态代码。推送服务之间的错误消息不同,有些比其他更有帮助。
在此示例中,它检查状态代码'404'和'410',它们是'Not Found'和'Gone'的HTTP状态代码。如果我们收到其中一个,则表示订阅已过期或不再有效。在这些场景中,我们需要从数据库中删除订阅。
处理推送事件
在用户的设备上接收此推送消息并显示通知
收到消息后,它将导致在服务工作者中调度推送事件。
self.addEventListener('push', function(event) {
if (event.data) {
console.log('This push event has data: ', event.data.text());
} else {
console.log('This push event has no data.');
}
});
self通常用于服务工作者的Web Workers。self指的是全局范围,类似于window网页。但对于网络工作者和服务工作者来说, self指的是工作者本身。
self.addEventListener()可以将其视为向服务工作者本身添加事件侦听器。
// Returns string
event.data.text()
// Parses data as JSON string and returns an Object
event.data.json()
// Returns blob of data
event.data.blob()
// Returns an arrayBuffer
event.data.arrayBuffer()
服务工作者需要了解的一点是,您几乎无法控制服务工作者代码何时运行。浏览器决定何时将其唤醒以及何时终止它。你可以告诉浏览器的唯一方法是“嘿,我忙着做重要的事情”,就是将一个承诺传递给event.waitUntil()方法。有了这个,浏览器将保持服务工作者运行,直到您传入的承诺结束。
对于推送事件,还有一个额外要求,即您必须在传递的承诺结算之前显示通知。
self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
event.waitUntil(promiseChain);
});
调用self.registration.showNotification()是向用户显示通知的方法,它返回一个在显示通知后将解析的承诺。
通过网络数据请求和使用分析跟踪推送事件的更复杂示例可能如下所示:
self.addEventListener('push', function(event) {
const analyticsPromise = pushReceivedTracking();
const pushInfoPromise = fetch('/api/get-more-data')
.then(function(response) {
return response.json();
})
.then(function(response) {
const title = response.data.userName + ' says...';
const message = response.data.message;
return self.registration.showNotification(title, {
body: message
});
});
const promiseChain = Promise.all([
analyticsPromise,
pushInfoPromise
]);
event.waitUntil(promiseChain);
});
这里我们调用一个返回promise的函数,pushReceivedTracking()为了示例,我们可以假装向我们的分析提供者发出网络请求。我们还发出网络请求,获取响应并使用响应数据显示通知的标题和消息。
我们可以确保服务工作者保持活力,同时通过将这些承诺与这些承诺相结合来完成这两项任务Promise.all()。产生的承诺将被转换为event.waitUntil() 意味着浏览器将等待两个承诺完成,然后再检查是否已显示通知并终止服务工作者。
我们应该关注waitUntil()以及如何使用它的原因是开发人员面临的最常见问题之一是,当承诺链不正确/损坏时,Chrome会显示此“默认”通知:
Chrome只会显示“此网站已在后台更新”。收到推送消息时的通知,并且服务工作者中的推送事件在传递到的承诺event.waitUntil()完成后未显示通知。
显示通知
显示通知的API是:
<ServiceWorkerRegistration>.showNotification(<title>, <options>);
标题是字符串,选项可以是以下任何一种:
{
"//": "Visual Options",
"body": "<String>",
"icon": "<URL String>",
"image": "<URL String>",
"badge": "<URL String>",
"vibrate": "<Array of Integers>",
"sound": "<URL String>",
"dir": "<String of 'auto' | 'ltr' | 'rtl'>",
"//": "Behavioural Options",
"tag": "<String>",
"data": "<Anything>",
"requireInteraction": "<boolean>",
"renotify": "<Boolean>",
"silent": "<Boolean>",
"//": "Both Visual & Behavioural Options",
"actions": "<Array of Strings>",
"//": "Information Option. No visual affect.",
"timestamp": "<Long>"
}
浏览器支持:
https://jakearchibald.github.io/isserviceworkerready/