h5的Notification 、web Push介绍

前言

本文是客户端和服务器端通信最后一个系列,主要介绍h5的桌面通知和web push,h5的Notification主要用于向用户展示通知,而web push 主要用于订阅推送消息。

h5的Notification

关于h5的Notification已经不是什么新的技术,但是最新chrome浏览器也是只有在https协议下面才有效。下面简单介绍一下Notification

语法
let myNotification = new Notification(title, options);
参数

一、 title
定义一个通知的标题,当它被触发时,它将显示在通知窗口的顶部。

二、 options 可选 options对象包含应用于通知的任何自定义设置选项。选项有:

dir: 显示通知的方向。默认是auto,跟随浏览器语言设置行为,你也可以通过设置ltr和rtl的值来覆盖该行为(虽然大多数浏览器似乎忽略这些设置)
lang: 通知的语言,如使用代表一个BCP 47语言标签的  DOMString 指定的。请参阅Sitepoint ISO 2字母语言代码页面,以获得简单的参考。
badge: 一个 USVString 包含用于表示通知的图像的URL, 当没有足够的空间来显示通知本身时。
body: 一个 DOMString 表示通知的正文,将显示在标题下方。
tag:  一个 DOMString 代表通知的 一个识别标签。
icon:  一个 USVString 包含要在通知中显示的图标的URL。
image: 一个 USVSTring包含要在通知中显示的图像的URL。
data: 您想要与通知相关联的任意数据。这可以是任何数据类型。
vibrate: 一个振动模式 vibration pattern 设备的振动硬件在通知触发时发出。
renotify: 一个 Boolean 指定在新通知替换旧通知后是否应通知用户。默认值为false,这意味着它们不会被通知。
requireInteraction: 表示通知应保持有效,直到用户点击或关闭它,而不是自动关闭。默认值为false。

以下选项列在最新规范中,但是目前浏览器可能尚未支持,大家可以随时尝试!

silent: 一个 Boolean 指明通知是否应该是无声的,即,不需要发出声音或振动,无论设备设置如何。默认值为false,这意味着它不会保持静默。
sound:一个 USVString 包含通知触发时要播放的音频文件的URL。
noscreen: 一个 Boolean 指定通知触发是否应启用设备的屏幕。 默认值为false,这意味着它将启用屏幕。
sticky: 一个 Boolean 指明通知是否应该是“粘”, 即不易被用户清理。默认值为false,这意味着它不会粘。

案例

function spawnNotification(theBody,theIcon,theTitle) {
  var options = {
      body: theBody,
      icon: theIcon
  }
  var n = new Notification(theTitle,options);
}

web push

用户订阅了一个站点的 Web Push 服务后,即使用户关闭了浏览器,一旦站点主动发送推送消息,用户都能收到,只要你的电脑是开着的。这是目前谷歌和苹果在 Chrome 和 Safari 上都力推的一种全新推送服务,Firefox最近也加入了这个阵营。

web push 优点

一、 可以完美替代原来的Email订阅服务,因为 Email 订阅这个动作要用户主动发起,不管你在页面内放了多显眼的订阅标志,都得用户去主动点击,填写自己 Email 地址才行。而且,如果不使用邮件客户端,很多订阅的 Email 发出去犹如石沉大海,到达率很低。

Web Push 完全由浏览器开发商的标准协议发起,一旦用户访问开通了 Web Push 的站点 ,浏览器就会主动询问你是否要订阅,弹出的订阅框也都是浏览器开发者的标准规范。首先,它增加了用户订阅的可能性。

二、 一旦用户点阅,只要你打开电脑,就会收到推送通知,没有邮件订阅时用户主动去查收邮件的问题,大大增加了订阅推送到达率。

三、 Web Push 让站点为主,APP 为辅的网站可以摆脱 APP 安装量少的困扰,访问你站点的人,只有一小部分会安装你的 APP,而Web Push 不需要安装任何 APP 在电脑上,只要用户点击一次Allow来订阅消息即可。

web push 国内使用少的原因

1、 Web Push 使用了 GCM 服务,而 GCM 在国内基本上很难连通,国内用户除非翻了,否则压根不可能订阅成功 Chrome 的 Web Push。

2、 谷歌要求 Chrome 的 Web Push 必须用 https 加密传输,国内目前很多网站还在使用http协议。
所以目前国内web push 使用较少。

web push 流程

1、客户端完成请求订阅一个用户的逻辑
2、服务端调用遵从web push协议的接口,传送消息推送(push message)到推送服务器(该服务器由浏览器决定,开发者所能做的只有控制发送的数据)
3、推送服务器将该消息推送至对应的浏览器,用户收到该推送

第一步, 客户端请求订阅用户,过程如下:

说明一下这三步,在第一步之前,应用服务器需要生成应用服务器密钥(application server keys),其作用是标识该服务器,保证每次发消息推送的都是同一个服务器。然后,客户端将会请求用户授权消息推送,一旦用户授权,浏览器就会生成一个PushScription,然后这个PushScription将会被发送至服务器,存入数据库,在后面的消息推送中使用。

第二步, 应用服务器发送web push协议标准的api,触发推送服务器的消息推送,其中headers必须配置正确,且传送的数据必须是比特流。

应用服务器发送消息推送请求(目的是为了将更新推送到用户的浏览器),为了向推送服务器发出请求,需要查看先前获得的PushScription,取出其中的endpoint,即为推送服务器配置给该用户的访问点。

一个PushScription对象如下:

{
  "endpoint": "https://random-push-service.com/some-kind-of-unique-id-1234/v2/",
  "keys": {
    "p256dh" :
"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=",
    "auth"   : "tBHItJI5svbpez7KI4CCXg=="
  }
}

其中的endpoint包含了推送服务器域名,path后面的部分为推送服务器为每个用户分配的一个标识符。

发送数据时,数据必须编码(出于安全性考虑)。推送服务器在接收到这样一个请求之后,立即开始监听用户浏览器是否处于在线状态,若是,则将消息推送发送至浏览器。

第三步, 浏览器端接收消息推送,触发push事件并展示

浏览器在接收到推送服务器发来的推送后,将其解码并触发一个push事件。Service Worker由于它可以在浏览器页面未打开,浏览器未打开时执行,因此一般选择它完成web push的最后一步,即响应push事件完成展示通知等业务逻辑。


web push实现细节

1、 按照上一部分所说,首先进行用户订阅。
首先注册一个Service Worker,若注册成功,返回的Promise为resolve状态,如下:

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,这方面的详细文章,大家可以自行网上搜索。)

2、 随后测试window环境下是否有Notification对象(此处以chrome为例,若使用firefox,uc等浏览器,需要遵循其相应标准,调用对应对象方法或引入JS SDK包),
测试成功,调用Notification.requestPermission请求用户授权发送推送,若授权成功,将会返回'granted'。

3、 接下来要做的就是使用注册好的Service Worker对象,调用pushManager.subscribe方法,从客户端获得刚刚所说的PushScription对象。

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;
  });
}
解释:
1、userVisibleOnly是为了保证推送对用户可见,

2、application server key则如前文所说,是推送服务器用以识别应用服务器的密钥,这里的密钥包含了公钥和私钥,传输的是公钥。

3、同时,PushScription的endpoint也是在这个过程中生成的,生成公钥和私钥可以使用web-push库。

这里再次说明一下推送服务器的不可选择性,在调用subscribe生成PushScription时,浏览器会向它指定的中转服务器发送请求来生成endpoint和其余部分,这是没法控制的。

4、PushScription中的auth和p256dh是用来控制带载荷的push message的。

4、 获取到PushScription对象后,将其发往应用服务器,此处简化了存储,使用nedb存下PushScription并返回Promise:

function 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库完成配置密钥及联系邮箱的示例:

const vapidKeys = {
  publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls'
};

webpush.setVapidDetails(
  'mailto:web-pust-test@liaobu.de',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

6、 不要忘了配置你在谷歌云服务(例如FCM)申请到的GCMApiKey:

webpush.setGCMAPIKey('<Your GCM API Key Here>');

7、 配置完成后,就可以将subscription发送出去,使用web-push的sendNotification接口:

webpush.sendNotification(pushSubscription, 'Your Push Payload Text');

推送服务器发送消息后,会触发浏览器的push事件,为了控制service worker的逻辑,需要使用event.waitUntil方法,此方法接收一个promise参数,在promise变为resolved状态后,浏览器就会检查通知是否已被展示,若是,则关闭service worker。

如果不处理未正常执行的promise,部分浏览器如chrome会展示默认消息框:

展示一个通知调用的为showNotification方法,传的参数包括title等,如下:

var title = 'Yay a message.';
var body = 'We have received a push message.';
var icon = '/images/icon-192x192.png';
var tag = 'simple-push-demo-notification-tag';

event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
);

而展示notification时,除了控制它的视图层以外,也可以控制它的逻辑层,
例如点击消息通知后进行某些操作等等,在先前调用showNotification时可以传入一些参数,
例如,根据不同的action执行不同的操作:

self.addEventListener('notificationclick', function(event) {
  if (!event.action) {
    // Was a normal notification click
    console.log('Notification Click.');
    return;
  }

  switch (event.action) {
    case 'coffee-action':
      console.log('User ❤️️\'s coffee.');
      break;
    case 'doughnut-action':
      console.log('User ❤️️\'s doughnuts.');
      break;
    case 'gramophone-action':
      console.log('User ❤️️\'s music.');
      break;
    case 'atom-action':
      console.log('User ❤️️\'s science.');
      break;
    default:
      console.log(`Unknown action clicked: '${event.action}'`);
      break;
  }
});
web-push请参见github: https://github.com/web-push-libs/web-push
兼容性及其他问题

与ajax轮询、http长连接、WebSocket的对比

  • ajax轮询是通过客户端不断向服务端发送http请求,若有新消息就取回的模式保持数据实时更新,但这种方式需要服务器有很快的处理速度和资源
  • http长连接是客户端向服务器发送请求后,若服务器没有新数据要发送,就不返回response,一旦有了新数据返回了response,客户端就立刻再发一个request,周而复始。事实上这是把http协议的不对称性从客户端转移到了服务端
  • WebSocket是HTML5中提出的一个新标准(也可视之为协议),客户端在发送请求时在请求头加入额外的字段,以标识这是一个基于WebSocket协议的连接,服务器根据这个请求头生成响应,与客户端建立起WebSocket连接,之后服务端有新消息时,直接向客户端推送即可

不同浏览器兼容性

  • chrome采用的推送服务器为gcm或fcm,firefox也有自己的推送服务器
  • uc前些时间构建了自己的推送服务器,引入其官网上的sdk包,申请使用后即可用于开发
这就是整一个过程,具体来说:
(1)浏览器发起询问,生成subscription

在注册完service worker后,调用subscribe询问用户是否允许接收通知,如下代码所示:

navigator.serviceWorker.register("sw-4.js").then(function(reg){
    console.log("Yes, it did register service worker.");
    if (window.PushManager) {
        reg.pushManager.getSubscription().then(subscription => {
            // 如果用户没有订阅
            if (!subscription) {
                subscribeUser(reg);
            } else {
                console.log("You have subscribed our notification");
            }       
        });     
    }
}).catch(function(err) {
    console.log("No it didn't. This happened: ", err)
});

上面代码在发起订阅前先看一下之前已经有没有订阅过了,如果没有的话再发起订阅。

发起订阅的subscribeUser实现如下代码所示:

function subscribeUser(swRegistration) {
    const applicationServerPublicKey = "BBlY_5OeDkp2zl_Hx9jFxymKyK4kQKZdzoCoe0L5RqpiV2eK0t4zx-d3JPHlISZ0P1nQdSZsxuA5SRlDB0MZWLw";
    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
    swRegistration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: applicationServerKey
    })
    // 用户同意
    .then(function(subscription) {
        console.log('User is subscribed:', JSON.stringify(subscription));
        jQuery.post("/add-subscription.php", {subscription: JSON.stringify(subscription)}, function(result) {
            console.log(result);
        });
    })
    // 用户不同意或者生成失败
    .catch(function(err) {
        console.log('Failed to subscribe the user: ', err);
    });
}

subscribe传两个参数,一个是userVisibleOnly,这个表示消息是否必须要可见,如果设置为不可见,Chrome将会报错:

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包生成,先安装一个:
npm install web-push --save

然后用以下代码生成:

const webpush = require('web-push');
//VAPID keys should only be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();
console.log(vapidKeys.publicKey, vapidKeys.privateKey);

每运行一次就会生成一对新的密钥对,如:

publicKey:  BMgkd1qfOfI6vFBbxIFMgdxDGC6-j8NYTwF_MXOIZ-St9lPhhMdPuUyFfwg1DLY59WP0FEaX84ZJRwgztdpfBHs
privateKey: LUeSF8DCv-NBxIfaeWeKTux858H45_V75vT0zZQLEbY

公密钥只要能配套就好,公钥在浏览器端使用,用来生成subscription,密钥在服务端使用,用来发Push。

如果用户同意浏览器就会向FCM服务请求生成subscription,然后执行Promise链里的then,返回该subscription,在这个then里面把这个subscription发给服务端存起来。
反之,如果用户不同意,或者用户无法连到FCM的服务器将会抛异常:DOMException: Registration failed - push service error

生成的subscription大概长这样:

{"endpoint":"https://fcm.googleapis.com/fcm/send/ci3-kIulf9A:APA91bEaQfDU8zuLSKpjzLfQ8121pNf3Rq7pjomSu4Vg-nMwLGfJSvkOUsJNCyYCOTZgmHDTu9I1xvI-dMVLZm1EgmEH0vDA7QFLjPKShG86W2zwX0IbtBPHEDLO0WgQ8OIhZ6yTnu-S","expirationTime":null,"keys":{"p256dh":"BAdAo6ldzRT5oCN8stqYRemoihPGOEJjrUDL6y8zhdA_swao_q-HlY_69mzIVobWX2MH02TzmtRWj_VeWUFMnXQ=","auth":"SS1PBnGwfMXjpJEfnoUIeQ=="}}
(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:

const webpush = require('web-push');
// 从数据库取出用户的subsciption
const pushSubscription = {"endpoint":"https://fcm.googleapis.com/fcm/send/ci3-kIulf9A:APA91bEaQfDU8zuLSKpjzLfQ8121pNf3Rq7pjomSu4Vg-nMwLGfJSvkOUsJNCyYCOTZgmHDTu9I1xvI-dMVLZm1EgmEH0vDA7QFLjPKShG86W2zwX0IbtBPHEDLO0WgQ8OIhZ6yTnu-S","expirationTime":null,"keys":{"p256dh":"BAdAo6ldzRT5oCN8stqYRemoihPGOEJjrUDL6y8zhdA_swao_q-HlY_69mzIVobWX2MH02TzmtRWj_VeWUFMnXQ=","auth":"SS1PBnGwfMXjpJEfnoUIeQ=="}};

// push的数据
const payload = {
    title: '一篇新的文章',
    body: '点开看看吧',
    icon: '/html/app-manifest/logo_512.png',
    data: {url: "https://www.rrfed.com"}
  //badge: '/html/app-manifest/logo_512.png'
};

webpush.sendNotification(pushSubscription, JSON.stringify(payload));

经实验,在大多数情况下这个延迟基本在1s以内,这边刚按下回车运行完,那边浏览器就收到了,但是有时候会发送失败(国内网络问题?)。
如果这个代码要在服务端运行的话,那么你应该需要一台香港的服务器。假如把发Push的数据和服务放在香港的服务器,需要发Push的时候由华北的服务器做个中转向这台服务器发请求。
只要用户能连上FCM那就可以愉快地发Push了,如果用户连不上那就没办法。

(4)接收推送消息

用运行在后台的Service Worker接收,监听push事件:

this.addEventListener('push', function(event) {
    console.log('[Service Worker] Push Received.');
    console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

    let notificationData = event.data.json();
    const title = notificationData.title;
    // 可以发个消息通知页面
    //util.postMessage(notificationData); 
    // 弹消息框
    event.waitUntil(self.registration.showNotification(title, notificationData));
});

主要是调用showNotification进行弹框,或者是使用postMessage通知页面相应地做些处理。
经实验,如果用户关闭了浏览器,在关闭期间如果有Push的话等到用户重新打开浏览器会再弹出来。
然后用户可以点击弹出来的框打开一个指定的页面,这个需要监听notificationclick事件:

this.addEventListener('notificationclick', function(event) {
    console.log('[Service Worker] Notification click Received.');

    let notification = event.notification;
    notification.close();
    event.waitUntil(
        clients.openWindow(notification.data.url)
    );
});

调用clients.openWindow打开一个新的页面,这样就基本完成了一个push推送的搭建。

Service Worker让我们在Web端也能有像原生APP一样的Push通知,使得Web端越来越像原生APP端,随着HTML5的其它新功能如WebAssembly提高运行速度,WebWorker多线程支持,数据库支持大量数据的管理和支持,Websocket进行实时通信,WebRTC进行P2P多媒体传输,还有WebGL、新进的WebVR等,使得在浏览器端能够做的事情越来越多,体验越来越丰富,而且这种Web APP还是跨平台的。

发表评论

邮箱地址不会被公开。 必填项已用*标注