边学边译JS工作机制---9 网页推送机制

420 阅读8分钟

本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)

推送机制在手机端已经非常普遍了,但奇怪的是在web上却出现的很迟,即使开发者呼吁这个功能很久了。

概述

网页推送机制,允许用户选择定时更新app。它旨在为用户重新获取其感兴趣,重要和及时的信息。

推送是基于Service Worker的。

使用Service Worker的原因,是他们是后台执行的。这对推送信息来说是非常重要的,因为这意味着只有当用户和推送通知本身进行交互操作才会执行推送通知的相关代码。

推送 & 通知

推送和通知是不同的API:

  • Push — 服务端发送信息给Service Worker时触发
  • Notification — 这是Service Worker或者一个脚本给用户推送信息。

推送

实现推送需要三个步骤

  1. UI — 增加必要的客户端逻辑订阅用户,便于push。在网络应用界面中书写 JavaScript 代码逻辑来让用户注册消息推送服务。
  2. 发送推送的消息 — 服务器实现的API调用,推送消息到用户设备
  3. 接受消息 — 浏览器接收到消息后就处理

稍后我们讨论更多细节

兼容性检测

首先,我们需要去检查是否当前的浏览器支持推送消息。我们可以通过两个步骤检测:

  1. 检查navigator对象是否有serviceWorker
  2. 检查 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 Woker. 你可能已经很熟悉这个流程了

请求权限

After a Service Worker has been registered, we can proceed with subscribing the user. To do so, we need to get his permission to send him push messages. Service Worker注册之后,下面需要订阅用户。为了这一步,我们需要获取向用户推送信息的权限。 获取权限的API是非常简单的,但是也有缺点,API从之前的回调变成了返回一个Promise。 这引入了一个问题,我们不知道当前浏览器实现了哪个版本,所以我们需要实现这两种方式。

像这样:

function requestPermission() {
  return new Promise(function(resolve, reject) {
    const permissionResult = Notification.requestPermission(function(result) {
      // Handling deprecated version with callback.
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  })
  .then(function(permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error('Permission not granted.');
    }
  });
}

调用 Notification.requestPermission() 将会给用户显示这个

image.png

当权限请求被同意,关闭或者阻塞,我们会相应地给出一个字符串类型的结果‘granted’‘default’ or ‘denied’

如果用户点击了Block,你的app将不会再请求获得权限,知道用户手动改变权限状态,“unblock” 你的app。这个选项隐藏在设置panel里。

使用PushManager订阅用户

Once we have our Service Worker registered and we’ve got permission, we can subscribe a user by calling registration.pushManager.subscribe() when you register your Service Worker. 一旦我们注册了Service Worker,取得了权限,就可以在注册服务器线程的时候调用registration.pushManager.subscribe()  订阅用户

代码如下(包括注册服务工作线程):

function subscribeUserToPush() {
  return navigator.serviceWorker.register('service-worker.js')
  .then(function(registration) {
    var subscribeOptions = {
      userVisibleOnly: true,
      applicationServerKey: btoa(
        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
      )
    };

    return registration.pushManager.subscribe(subscribeOptions);
  })
  .then(function(pushSubscription) {
    console.log('PushSubscription: ', JSON.stringify(pushSubscription));
    return pushSubscription;
  });
}

registration.pushManager.subscribe(options)  传入一个options 对象,对象包含了可选和必要的属性

  • userVisibleOnly: 一个布尔值,表示返回的推送订阅只会用到用户可见的消息上。比如为true,否则会异常。(历史原因)
  • applicationServerKey:一个Base64编码的DOMString 或者 ArrayBuffer,包含了一个认证应用服务的推送服务的公钥。

你的服务器需要生成一对应用服务密钥 — ,他们是VAPID秘钥,对服务端来说是唯一的。 他们是一对儿,包含了公钥和私钥。私钥保存在推送服务端,公钥则用来和客户端交换数据。密钥允许一个推送服务知道哪个应用服务订阅了用户,并确保推送消息到指定用户的是同一个应用服务器。

你只需要为你的应用创建一次密钥。

当订阅用户时,浏览器传递了 applicationServerKey(公钥)到 推送服务上,这样推送服务就可以绑定你的公钥到用户的PushSubscription 看看发生了什么:

  • 应用加载,然后调用subscribe() ,传递服务公钥
  • 浏览器向推送服务发起一个请求,生成一个和密钥结合的终结点,然后返回这个终结点到浏览器
  • 浏览器添加这个终结点到subscribe()返回的PushSubscription对象

之后,无论何时你想发送一个推送消息,都需要创建一个包含签名信息的鉴权头,这个签名包含了服务器私钥。当推送服务接收到发送消息的请求,将会查询公钥去验证这个头信息---这个公钥在第二步已经连接到终结点了

PushSubscription 对象

PushSubscription 包含了所有有的需要给用户设备发送推送消息的相关信息。 看起来是这样

{
    "endpoint": "https://domain.pushservice.com/some-id",\
        "keys": {                              "p256dh":"BIPUL12DLfytvTajnryr3PJdAgXS3HGMlLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WArAPIxr4gK0_dQds4yiI=",\
        "auth":"FPssMOQPmLmXWmdSTdbKVw=="\
    }
} 

endpoint 是推送服务的URL。为了推送信息,发送一个POST请求到这个URL

keys 对象包含了用来加密数据的值,这个数据是随推送消息一起发送的

一旦用户被订阅了,就需要发送PushSubscription到服务器。在服务器,你可以在数据库保存订阅数据, 现在开始你就可以发送推送消息给用户

image.png

发送推送消息

如果想给你的用户发送推送消息,第一件事是你需要一个推送服务。你告知服务器需要发送什么数据,消息发给谁,以及如何发送信息的一切细节。一般情况下,这些接口调用是由消息推送服务器来完成的。

推送服务

消息推送服务是用来接收消息推送请求,验证请求以及推送消息到合适的浏览器端。

注意推送服务不是你管理的,它是第三方的服务。你的服务跟推送服务通过API通信。看一个推送服务的例子 Google’s FCM.

推送服务处理了所有麻烦的事。例如,假如浏览器离线了,在发送消息之前,推送服务将会让消息排队挂起,直到浏览下恢复上线

每一个浏览器可以使用任意推送服务,但这是开发者不能掌控的事情了。

不过,所有的推送服务具有相同的API,这样实现起来就不会太痛苦。

为了获取处理推送消息的请求的URL, 你需要检查PushSubscription 对象中endpoint 存储的值。

推送服务API

推送服务API提供了给用户发送消息的方式,这个API被称为 Web Push Protocol,它是根据IEFT规范定义的,这个规范定义了如何调用推送API。 你发送的推送消息,一定要加密。这样,防止推送服务看到发送的数据。这个很重要,因为浏览器决定了使用哪个推送服务(可能会使用一些不被信任和不安全的消息推送服务)

推送消息的说明如下:

  • TTL — 定了在被删除或者分发之前,一个消息需要排队多长时间。
  • Priority — 定义了每一个消息的优先级。推送服务将值发送优先级高的消息,以便保护用户的电池生命
  • Topic —给推送消息定义专题名,新消息会替换掉挂起消息中同样专题名的内容,这样一旦设备激活,用户就不会收到过期的信息。

image.png

浏览器的推送事件

一旦我们按上面说的,发送信息到推送服务器,消息将会处于挂起状态,直到下面情况发生

  • 设备上线
  • 根据TTL,队列中的消息过期

当推送服务分发了一个消息,浏览器将会接收,解密,然后在service worker中分发一个push事件

这里有一个比较牛逼的事情,即使你的页面没有打开,浏览器还是可以执行你的ServiceWorker。会发生如下事件:

  • 推送服务达到浏览器,浏览器解密
  • 浏览器唤醒Service Worker
  • 触发push事件到Service Worker

监听推送事件和在 JavaScript 中写的其它事件监听非常类似

self.addEventListener('push', function(event) {
  var promise = self.registration.showNotification('Push notification!');

  event.waitUntil(promise);
});

需要理解Service Workers的一点即其运行时间是不可人为控制的。只有浏览器可以唤醒和结束它。

Service Workers中,event.waitUntil(promise) 告知浏览器持续工作,直到promise完成。如果想完成Service Workers,浏览器就不应该终结它。 看一个处理push 事件的例子:

self.addEventListener('push', function(event) {
  var promise = self.registration.showNotification('Push notification!');

  event.waitUntil(promise);
});

调用 self.registration.showNotification() 向用户弹出一个通知并且返回一个 promise,一旦通知显示完成即解析完成。

可以采用可视化的方法来设置符合自己需求的 showNotification(title, options) 方法。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>"\
}

更多细节— developer.mozilla.org/en-US/docs/….

当有重要,紧急,实时的信息需要分享给用户,那么推送通知是获取用户注意力的好办法,