【前端】Web Notifications API 和 Web Push API(通知接口)
关于在某黄色网站的桌面通知广告上学习到的知识
事情是这样的,今天我在写代码的时候,伴随着一声系统提示音,右下角突然弹出来了某不知名黄色网站大尺度的广告桌面通知。将此情景拍下来后发到程序猿群求助沙雕网友怎么彻底关闭,结果引 LSP 们的围观,毁了我的一世英名。
删除该网站通知权限
该桌面消息是通过 Microsoft Edge 发出的,但是明明我就没打开 Edge,并且从来没有浏览过该不知名网站,确信!(`へ´*)ノ
群里 Eric_Lian 提醒我可以去 Edge 设置里检查下有没有奇怪的通知网站,可能是点了一些同意通知导致的。找了一下确实有,然后果断把它移除了:
电脑清净了。
OK,现在进入学习阶段,了解并学会使用这种系统消息的实现方法(不愧是我)。
小黄网:喂!你们不看我的广告推送竟然去学技术,焯!竟输得这么彻底!
Web Notifications API
群里 保罗 说道,Notifications接口就是弹系统通知的,但首次打开需授权,类似安卓应用。
1. Notifications API概述
Notification API是浏览器的通知接口,用于在用户的桌面(而不是网页上)显示通知信息,桌面电脑和手机都适用,比如通知用户收到了一封Email。具体的实现形式由浏览器自行部署,对于手机来说,一般显示在顶部的通知栏。
如果网页代码调用这个API,浏览器会询问用户是否接受。只有在用户同意的情况下,通知信息才会显示。目前,Chrome和Firefox在桌面端部署了这个API,Firefox和Blackberry在手机端部署了这个API。
下面的代码用于检查浏览器是否支持这个API。
1 | if (window.Notification) { |
下面代码检查当前浏览器是否支持Notification对象,并且当前用户准许使用该对象,然后调用Notification.requestPermission方法,向用户弹出一条通知。
1 | if(window.Notification && Notification.permission !== "denied") { |
2. Notification对象的属性和方法
(1) Notification.permission
Notification.permission属性,用于读取用户给予的权限,它是一个只读属性,它有三种状态。
default
:用户还没有做出任何许可,因此不会弹出通知。granted
:用户明确同意接收通知。denied
:用户明确拒绝接收通知。
1 | // 查看站点通知权限状态 |
(2) Notification.requestPermission()
Notification.requestPermission方法用于让用户做出选择,到底是否接收通知。它的参数是一个回调函数,该函数可以接收用户授权状态作为参数。
1 | Notification.requestPermission(function (status) { |
上面代码表示,如果用户拒绝接收通知,可以用alert方法代替。
(3) Notification实例对象
① Notification构造函数
Notification对象作为构造函数使用时,用来生成一条通知。
1 | var notification = new Notification(title, options); |
Notification构造函数的title属性是必须的,用来指定通知的标题,格式为字符串。options属性是可选的,格式为一个对象,用来设定各种设置。该对象的属性如下(这些属性都是可读写的):
dir
:文字方向,可能的值为auto、ltr(从左到右)和rtl(从右到左),一般是继承浏览器的设置。lang
:使用的语种,比如en-US、zh-CN。body
:通知内容,格式为字符串,用来进一步说明通知的目的。。tag
:通知的ID,格式为字符串。一组相同tag的通知,不会同时显示,只会在用户关闭前一个通知后,在原位置显示。icon
:图标的URL,用来显示在通知上。image
:图像的URL,用来显示在通知上。requireInteraction
:是否常驻,默认值为false
。表示通知应保持有效,直到用户点击或关闭它,而不是自动关闭。silent
: 是否静音,默认值为false
,这意味着它不会保持静默。指明通知是否应该是无声的,即,不需要发出声音或振动,无论设备设置如何。
更多属性可参考 MDN文档 | Notification,部分属性尚在实验中,浏览器可能尚未支持。
下面是一个生成Notification实例对象的例子。
1 | var notification = new Notification('收到新邮件', { |
写完整一点,带上图标:
1 | var notification = new Notification('百里飞洋(Github)', { |
再完整一点,带上图片:
1 | var notification = new Notification('百里飞洋(Github)', { |
② 实例对象的事件
Notification实例会触发以下事件。
show
:通知显示给用户时触发。click
:用户点击通知时触发。close
:用户关闭通知时触发。error
:通知出错时触发(大多数发生在通知无法正确显示时)。
这些事件有对应的onshow、onclick、onclose、onerror方法,用来指定相应的回调函数。addEventListener方法也可以用来为这些事件指定回调函数。
1 | notification.onshow = function() { |
测试了一下,其中
onclose
函数不知为何没有起作用。
③ close方法
Notification实例的close方法用于关闭通知。
1 | var n = new Notification("Hi!"); |
上面代码说明,并不能从通知的close事件,判断它是否为用户手动关闭。
3. 完整实现方法代码
完整的桌面通知实现代码示例:
1 | // 封装通知函数 |
Web Push API
群里 戴兜丫 说道,如果我没有主动访问过该网站的话,估计是push接口。跟通知类似,但可以在网页没打开的情况下由服务端主动推送。
于是去了解了一下 Web Push :
以下内容摘自其他博文,技术内容未经验证,请谨慎阅读。(原文:h5的Notification 、web Push介绍)
用户订阅了一个站点的 Web Push 服务后,即使用户关闭了浏览器,一旦站点主动发送推送消息,用户都能收到,只要你的电脑是开着的。这是目前谷歌和苹果在 Chrome 和 Safari 上都力推的一种全新推送服务,Firefox最近也加入了这个阵营。
1. Web Push 优点
可以完美替代原来的Email订阅服务,因为 Email 订阅这个动作要用户主动发起,不管你在页面内放了多显眼的订阅标志,都得用户去主动点击,填写自己 Email 地址才行。而且,如果不使用邮件客户端,很多订阅的 Email 发出去犹如石沉大海,到达率很低。Web Push 完全由浏览器开发商的标准协议发起,一旦用户访问开通了 Web Push 的站点 ,浏览器就会主动询问你是否要订阅,弹出的订阅框也都是浏览器开发者的标准规范。首先,它增加了用户订阅的可能性。
一旦用户点阅,只要你打开电脑,就会收到推送通知,没有邮件订阅时用户主动去查收邮件的问题,大大增加了订阅推送到达率。
Web Push 让站点为主,APP 为辅的网站可以摆脱 APP 安装量少的困扰,访问你站点的人,只有一小部分会安装你的 APP,而 Web Push 不需要安装任何 APP 在电脑上,只要用户点击一次Allow来订阅消息即可。
2. 国内使用少的原因
下面两点未证实,国内也有正常使用的方式,比如 WebSocket 这个新的协议,它是HTTP的加强版,它允许服务端主动向客户端推送数据。或者 Service Worker 也可以发送 PUSH 推送。
Web Push 使用了 GCM 服务,而 GCM 在国内基本上很难连通,国内用户除非翻了,否则压根不可能订阅成功 Chrome 的 Web Push。
谷歌要求 Chrome 的 Web Push 必须用 https 加密传输,国内目前很多网站还在使用http协议。所以目前国内web push 使用较少。
3. Web Push 流程
- 客户端完成请求订阅一个用户的逻辑
- 服务端调用遵从web push协议的接口,传送消息推送(push message)到推送服务器(该服务器由浏览器决定,开发者所能做的只有控制发送的数据)
- 推送服务器将该消息推送至对应的浏览器,用户收到该推送
第一步, 客户端请求订阅用户,过程如下:
说明一下这三步,在第一步之前,应用服务器需要生成应用服务器密钥(application server keys),其作用是标识该服务器,保证每次发消息推送的都是同一个服务器。然后,客户端将会请求用户授权消息推送,一旦用户授权,浏览器就会生成一个PushScription,然后这个PushScription将会被发送至服务器,存入数据库,在后面的消息推送中使用。
第二步, 应用服务器发送web push协议标准的api,触发推送服务器的消息推送,其中headers必须配置正确,且传送的数据必须是比特流。
应用服务器发送消息推送请求(目的是为了将更新推送到用户的浏览器),为了向推送服务器发出请求,需要查看先前获得的PushScription,取出其中的endpoint,即为推送服务器配置给该用户的访问点。
一个PushScription对象如下:
1 | { |
其中的endpoint包含了推送服务器域名,path后面的部分为推送服务器为每个用户分配的一个标识符。
发送数据时,数据必须编码(出于安全性考虑)。推送服务器在接收到这样一个请求之后,立即开始监听用户浏览器是否处于在线状态,若是,则将消息推送发送至浏览器。
第三步,浏览器端接收消息推送,触发push事件并展示
浏览器在接收到推送服务器发来的推送后,将其解码并触发一个push事件。Service Worker由于它可以在浏览器页面未打开,浏览器未打开时执行,因此一般选择它完成web push的最后一步,即响应push事件完成展示通知等业务逻辑。
4. Web Push实现细节
1.按照上一部分所说,首先进行用户订阅。首先注册一个Service Worker,若注册成功,返回的Promise为
resolve
状态,如下:1
2
3
4
5
6
7
8
9
10function 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,可以查看 MDN文档 | Service Worker API
2.随后测试window环境下是否有Notification对象(此处以chrome为例,若使用firefox,uc等浏览器,需要遵循其相应标准,调用对应对象方法或引入JS SDK包)。测试成功,调用
Notification.requestPermission
请求用户授权发送推送,若授权成功,将会返回granted
。3.接下来要做的就是使用注册好的Service Worker对象,调用pushManager.subscribe方法,从客户端获得刚刚所说的PushScription对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function 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;
});
}- userVisibleOnly是为了保证推送对用户可见,
- application server key则如前文所说,是推送服务器用以识别应用服务器的密钥,这里的密钥包含了公钥和私钥,传输的是公钥。
- 同时,PushScription的endpoint也是在这个过程中生成的,生成公钥和私钥可以使用web-push库。这里再次说明一下推送服务器的不可选择性,在调用subscribe生成PushScription时,浏览器会向它指定的中转服务器发送请求来生成endpoint和其余部分,这是没法控制的。
- PushScription中的auth和p256dh是用来控制带载荷的push message的。
4.获取到PushScription对象后,将其发往应用服务器,此处简化了存储,使用nedb存下PushScription并返回Promise:
1
2
3
4
5
6
7
8
9
10
11function saveSubscriptionToDatabase(subscription) {
return new Promise(function(resolve, reject) {
db.insert(subscription, function(err, newDoc) {
if (err) {
reject(err);
return;
}
resolve(newDoc._id);
});
});
};5.存储完毕后,接下来就是开发后台管理逻辑,使得管理员能够触发向用户推送消息的事件,应用服务器所做的逻辑就是遍历在数据库中存储的所有PushScription并推送消息,以下是使用web-push库完成配置密钥及联系邮箱的示例:
1
2
3
4
5
6
7
8
9
10
11const vapidKeys = {
publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls'
};
webpush.setVapidDetails(
'mailto:web-pust-test@liaobu.de',
vapidKeys.publicKey,
vapidKeys.privateKey
);6.不要忘了配置你在谷歌云服务(例如FCM)申请到的GCMApiKey:
1
webpush.setGCMAPIKey('<Your GCM API Key Here>');
7.配置完成后,就可以将subscription发送出去,使用web-push的sendNotification接口:
1
webpush.sendNotification(pushSubscription, 'Your Push Payload Text');
推送服务器发送消息后,会触发浏览器的push事件,为了控制service worker的逻辑,需要使用event.waitUntil方法,此方法接收一个promise参数,在promise变为resolved状态后,浏览器就会检查通知是否已被展示,若是,则关闭service worker。
如果不处理未正常执行的promise,部分浏览器如chrome会展示默认消息框。
展示一个通知调用的为showNotification方法,传的参数包括title等,如下:
1 | var title = 'Yay a message.'; |
而展示notification时,除了控制它的视图层以外,也可以控制它的逻辑层,
例如点击消息通知后进行某些操作等等,在先前调用showNotification时可以传入一些参数,
例如,根据不同的action执行不同的操作:
1 | self.addEventListener('notificationclick', function(event) { |
5. 与其他技术的对比
与ajax轮询、http长连接、WebSocket的对比:
- ajax轮询是通过客户端不断向服务端发送http请求,若有新消息就取回的模式保持数据实时更新,但这种方式势必会对服务端造成很大压力,浪费带宽和服务器资源,需要服务器有很快的处理速度和资源。
- http长连接是客户端向服务器发送请求后,若服务器没有新数据要发送,就不返回response,一旦有了新数据返回了response,客户端就立刻再发一个request,周而复始。事实上这是把http协议的不对称性从客户端转移到了服务端
- WebSocket是HTML5中提出的一个新标准(也可视之为协议),客户端在发送请求时在请求头加入额外的字段,以标识这是一个基于WebSocket协议的连接,服务器根据这个请求头生成响应,与客户端建立起WebSocket连接,之后服务端有新消息时,直接向客户端推送即可
6. 不同浏览器的兼容性
- chrome采用的推送服务器为gcm或fcm,firefox也有自己的推送服务器
- uc前些时间构建了自己的推送服务器,引入其官网上的sdk包,申请使用后即可用于开发
7. 实现过程复盘
这就是整一个过程,具体来说:
- (1)浏览器发起询问,生成subscription。
在注册完service worker后,调用subscribe询问用户是否允许接收通知,如下代码所示:
1 | navigator.serviceWorker.register("sw-4.js").then(function(reg){ |
上面代码在发起订阅前先看一下之前已经有没有订阅过了,如果没有的话再发起订阅。
发起订阅的subscribeUser实现如下代码所示:
1 | function subscribeUser(swRegistration) { |
subscribe传两个参数,一个是userVisibleOnly,这个表示消息是否必须要可见,如果设置为不可见,Chrome将会报错:
1 | 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 for more details. |
但其实这个并不影响,我们设置成true,但是收到消息后可以不用弹框,可以调postMessage去通知页面做相应的操作。
第二个参数applicationServerKey是服务端的公钥,这个可以用web push的Node包生成,先安装一个:
1 | npm install web-push --save |
然后用以下代码生成:
1 | const webpush = require('web-push'); |
每运行一次就会生成一对新的密钥对,如:
1 | publicKey: BMgkd1qfOfI6vFBbxIFMgdxDGC6-j8NYTwF_MXOIZ-St9lPhhMdPuUyFfwg1DLY59WP0FEaX84ZJRwgztdpfBHs |
公密钥只要能配套就好,公钥在浏览器端使用,用来生成subscription,密钥在服务端使用,用来发Push。
如果用户同意浏览器就会向FCM服务请求生成subscription,然后执行Promise链里的then,返回该subscription,在这个then里面把这个subscription发给服务端存起来。反之,如果用户不同意,或者用户无法连到FCM的服务器将会抛异常:DOMException: Registration failed - push service error
生成的subscription大概长这样:
1 | { |
- (2)说了这么久的FCM,FCM到底是什么呢?
FCM官方是这么介绍的:Firebase 云信息传递 (FCM) 是一种跨平台消息传递解决方案,可供您免费、可靠地传递消息。
使用 FCM,您可以通知客户端应用存在可同步的新电子邮件或其他数据。您可以发送通知消息以再次吸引用户并促进用户留存。在即时消息传递等使用情形中,一条消息可将最大 4KB 的有效负载传送至客户端应用。
FCM是一种可靠的消息传递平台,它最大的优点是同一套Push机制可以在IOS/Android/Web三端使用:
这个意义是很大的,因为Android的推送一直都比较乱,国内有些APP使用小米的Push服务,有些使用百度的,还有些使用腾讯的信鸽等等,这些Push都需要在后台运行线程,并且不能休眠,这就导致了手机在休眠状态时仍然有很多线程在运行着,使得手机耗电速度很快。最后还直接导致今年工信部出台要成立安卓统一推送联盟。
而苹果有一套统一的推送机制,大家把Push发给苹果的服务器,然后再由苹果下发给相应的苹果设备。Safari现在不支持Service Worker,但是可以用Apple Push,缺点是这种推送苹果说不能用来发送重要的数据,并且目测只能弹框显示,没办法在后台处理消息而不弹框。
- (3)发送推送
发送推送可以用FCM提供的web push的库,它支持多种语言,包括Node.js/PHP等版本。
用Node.js可以这样发Push:
1 | const webpush = require('web-push'); |
经实验,在大多数情况下这个延迟基本在1s以内,这边刚按下回车运行完,那边浏览器就收到了,但是有时候会发送失败(国内网络问题?)。
如果这个代码要在服务端运行的话,那么你应该需要一台中国香港的服务器。假如把发Push的数据和服务放在中国香港的服务器,需要发Push的时候由华北的服务器做个中转向这台服务器发请求。
只要用户能连上FCM那就可以愉快地发Push了,如果用户连不上那就没办法。
- (4)接收推送消息
用运行在后台的Service Worker接收,监听push事件:
1 | this.addEventListener('push', function(event) { |
主要是调用showNotification进行弹框,或者是使用postMessage通知页面相应地做些处理。经实验,如果用户关闭了浏览器,在关闭期间如果有Push的话等到用户重新打开浏览器会再弹出来。然后用户可以点击弹出来的框打开一个指定的页面,这个需要监听notificationclick事件:
1 | this.addEventListener('notificationclick', function(event) { |
调用clients.openWindow打开一个新的页面,这样就基本完成了一个push推送的搭建。
Service Worker让我们在Web端也能有像原生APP一样的Push通知,使得Web端越来越像原生APP端,随着HTML5的其它新功能如WebAssembly提高运行速度,WebWorker多线程支持,数据库支持大量数据的管理和支持,Websocket进行实时通信,WebRTC进行P2P多媒体传输,还有WebGL、新进的WebVR等,使得在浏览器端能够做的事情越来越多,体验越来越丰富,而且这种Web APP还是跨平台的。
参考内容
这篇内容其实对我来说挺陌生,有时间慢慢消化。
总结一下,这篇博文的诞生和群里的化学反应是一次彻彻底底由小黄网引发的技术探讨。
让我们一起说:谢谢你小黄网!因为有你,温暖了四季。因为有你,世界更美丽!
[1]W3cSchool, Web Notifications API(通知接口)
[2]Aurelio De Rosa, An Introduction to the Web Notifications API
[3]MDN, Notification
[4]wo, h5的Notification 、web Push介绍
[5]MDN, Push API
[6]MDN, Service Worker API
[7]程序那点事, 7种实现web实时消息推送的方案
[8]repetition, Swoole 实现主动推送
[9]blackdous, 一文了解Service Worker
[10]有道AI情报局, 网易云课堂 Service Worker 运用与实践
[11]灰信网, 使用SERVICE WORKER发送PUSH推送