PWA系列 -- 带你认识Web Push技术

PWA

2018-11-26

# 前言

我们在之前推送的文章中为读者们分享了 PWA 这项技术,并有专门的文章对 PWA 技术的关键 -- Service Worker -- 做了单独的分享。那么除了离线存储的能力外,消息推送的能力也是 Service Worker 的关键能力,这项能力关系到 Web App 是否有能力向用户推送消息,这也是当前应用中必不可少的能力。今天,我们会为读者分享 Service Worker 的推送技术 -- Web Push。

# 基本概念

(1)webapp 网页应用程序,是指使用web技术实现的应用程序,在web浏览器或其它web运行环境的上下文中执行。 (2)application server 网页应用服务器,是指webapp的服务器端。 (3)push message 推送消息,是指application server发送给webapp的消息。application server在推送消息给推送服务(例如,GCM)时,会带上push subscription,推送服务会找到与push subscription相关联的active worker ,然后把消息推送给它,并触发它的onpush。 (4)push subscription

推送订阅,是代表webapp在用户代理(例如,浏览器)和推送服务(例如,GCM)之间建立的消息传递上下文。 每个push subscription都与一个service worker registration相关联,而一个service worker registration最多 只有一个push subscription。 每个push subscription都有一个push endpoint和一个公钥,公钥是用于给application server加密要推送的消息数据。

(5)push endpoint push endpoint,在ServiceWorker向push service注册时,由push service生成。在Chrome 45版本之前,push endpoint是指推送服务的地址,例如,GCM服务器的地址。在Chrome 45及之后版本,push endpoint = push server address + subscriptionId,其中subscriptionId可以唯一标识push subscription。在web客户端,通过subscriptionId可以找到相应的app_id和sender_id。app_id包含作用域和workerId,例如,push#http://example.com/test/#1。 (6)sender_id sender_id,是创建Google API项目时生成的项目ID。ServiceWorker通过web客户端向GCM注册订阅消息时,需要上传sender_id,才能订阅成功。GCM会使用sender_id,通过Instance ID API到Instance ID Service去注册生成Instance ID,此步骤最终生成的注册信息,也就是前面提到的 subscriptionId。注:Chrome52之前版本对应的GCM/FCM还不支持标准的Web Push Protocol,所以才需要通过gcm_sender_id去找到相应的web客户端,Chrome52版本及其对应的GCM/FCM已支持标准的Web Push Protocol,不再需要sender_id了,即支持标准的Web Push Protocol的push服务器都是不需要sender_id的。 (7)Subscription Refreshes web客户端或推送服务,可以在任何时间更新push subscription。更新成功后,必须触发与push subscription相关联的service worker registration的pushsubscriptionchange事件,如果更新失败,web客户端应定期触发更新,直至成功。 (8)Subscription Deactivation push subscription停止使用时,web客户端和推送服务必须删除已保存的副本,相关消息不再推送。service worker registration在注销或清除注册信息时,相关联的push subscription会被停用。 (9)Push service 推送服务,是指一个推送服务系统,允许application server通过它推送消息给webapp。

# Push流程

undefined (1)页面通过web客户端注册一个ServiceWorker,web客户端生成对应的ServiceWorkerRegistration对象。 (2)页面通过ServiceWorker的PushManager.subscribe方法向push service(GCM)订阅消息,GCM 返回push subscription (endpoint+subscriptionId)给web客户端,web客户端找到对应的ServiceWorker,并把push subscription传递给ServiceWorker。 (3)页面通过PushManager.getSubscription获取到push subscription,并将它发送给页面服务器。注,可以使用ajax请求发送数据给页面服务器。 (4)页面服务器需要推送消息时,将push message和push subscription发送给push service(例如,GCM)。GCM根据push subscription找到对应的web客户端并把消息推送给它,web客户端通过app_id找到相应的ServiceWorker,激活和把消息传递给它并触发它的onpush。 (5)页面JS根据实际需要去处理onpush事件,比如,弹出消息通知,存储数据到本地,提前到服务器去fetch资源,等等。 (6)页面在不再需要订阅消息时,可以发送ajax请求通知页面服务器去清除push subscription。然后使用PushSubscription.unsubscribe向push service去取消订阅。 基本流程的示例代码:

// https://example.com/serviceworker.js
this.onpush = function(event) {
  console.log(event.data);
  // From here we can write the data to IndexedDB, send it to any open
  // windows, display a notification, etc.
}
 
// https://example.com/webapp.js
navigator.serviceWorker.register('serviceworker.js').then(
  function(serviceWorkerRegistration) {
    serviceWorkerRegistration.pushManager.subscribe().then(
      function(pushSubscription) {
        console.log(pushSubscription.endpoint);
        console.log(pushSubscription.getKey('p256dh'));
        console.log(pushSubscription.getKey('auth'));
        // The push subscription details needed by the application
        // server are now available, and can be sent to it using,
        // for example, an XMLHttpRequest.
      }, function(error) {
        // During development it often helps to log errors to the
        // console. In a production environment it might make sense to
        // also report information about errors back to the
        // application server.
        console.log(error);
      }
    );
  });

# Push服务

(1)GCM/FCM undefined 注1:页面服务器(app server)使用HTTP/XMPP协议发送消息给Google GCM Connection Servers,GCM Connection Server 推送消息给相应的web客户端。 注2:页面服务器(app server)发送消息的格式如下,

curl --header "Authorization: key=<YOUR_API_KEY>" --header "Content-Type: application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"<YOUR_REGISTRATION_ID>\"]}"

其中,API_KEY 是创建Google API项目时生成的Server Key,GCM可通过它找到对应的sender_id。YOUR_REGISTRATION_ID 是订阅消息时生成的subscriptionId。 注3:Google Chrome是第一个支持Push API的浏览器,最先支持的版本为Chrome 42,这个时候Web Push Protocol还未完稿,所以其push service使用的GCM/FCM服务器并不支持标准Web Push Protocol。Chrome 52版本实现了对标准Web Push Protocol的支持,此时,GCM/FCM也已支持标准Web Push Protocol。 (2)第三方Push服务 Push API标准是允许替换endpoint的,即任何实现了Web Push Protocol的服务器都可作为endpoint,并且对使用者是透明的。 那么需要做哪些工作来实现我们自己的push服务呢? 我们先看看Web Push Protocol定义了那些内容: webpush architecture.png

  1. web客户端连接push service 需要使用https。
  2. web客户端订阅push message web客户端要发送POST请求,到push service去订阅消息。比如,
	POST /subscribe HTTP/1.1
    Host: push.example.net

订阅成功后push service的响应如下,

HTTP/1.1 201 Created
Date: Thu, 11 Dec 2014 23:56:52 GMT
Link: </push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV>;
                rel="urn:ietf:params:push"
Link: </subscription-set/4UXwi2Rd7jGS7gp5cuutF8ZldnEuvbOy>;
                rel="urn:ietf:params:push:set"
Location: https://push.example.net/subscription/LBhhw0OohO-Wl4Oi971UG

这个步骤中,push service需要能够根据不同情况返回不同的响应码,能够识别出同一web客户端的请求。 3.页面服务器发送推送消息 application server发送POST请求给push service,请求它将消息推送给web客户端。请求如下,

POST /push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV HTTP/1.1
Host: push.example.net
TTL: 15
Content-Type: text/plain;charset=utf8
Content-Length: 36

iChYuI3jMzt3ir20P8r_jgRR-dSuN182x7iB

push service的响应如下,

HTTP/1.1 201 Created
Date: Thu, 11 Dec 2014 23:56:55 GMT
Location: https://push.example.net/message/qDIYHNcfAIPP_5ITvURr-d6BGt

这个步骤中,push service需要能够根据不同情况返回不同的响应码,能够根据TTL保存推送消息一段时间并且含有Topic 头部的新消息能替换旧消息,能够根据Urgency头部决定推送的优先级。 4.web客户端接收订阅的推送消息 web客户端发GET请求请求接收推送消息,请求信息如下,

    HEADERS [stream 7] +END_STREAM +END_HEADERS
    :method = GET
    :path = /subscription/LBhhw0OohO-Wl4Oi971UG
    :authority = push.example.net

push service允许保持请求,在application server发送推送消息之后,它将消息通过HTTP/2 server push将消息推送给web客户端,响应信息如下,

    PUSH_PROMISE [stream 7; promised stream 4] +END_HEADERS
    :method = GET
    :path = /message/qDIYHNcfAIPP_5ITvURr-d6BGt
    :authority = push.example.net
    HEADERS [stream 4] +END_HEADERS
    :status = 200
    date = Thu, 11 Dec 2014 23:56:56 GMT
    last-modified = Thu, 11 Dec 2014 23:56:55 GMT
    cache-control = private
    link = </push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV>;
                 rel="urn:ietf:params:push"
    content-type = text/plain;charset=utf8
    content-length = 36
    DATA [stream 4] +END_STREAM
    iChYuI3jMzt3ir20P8r_jgRR-dSuN182x7iB
    HEADERS [stream 7] +END_STREAM +END_HEADERS
    :status = 200

web客户端接收到消息后,必须回应一个HTTP DELETE给push service,

DELETE /message/qDIYHNcfAIPP_5ITvURr-d6BGt HTTP/1.1
Host: push.example.net

push service在收到web客户端的回应后,必须返回一个204的响应给application server。如果没有收到回应,则会认为消息还未派发到web客户端, 那么push service应该继续重试,直至消息过期。 5.安全隐私方面的考虑 push服务需要考虑安全相关的一些问题,比如推送的消息需要进行加密,用户设备信息不应包含在消息推送中,过多异常请求时能够拒绝服务, 服务器日志不应暴露用户信息,等等。 从上面可以看看,push service至少需要具备的一些能力,比如,支持https,支持http/2 server push,支持POST,GET,DELETE请求,能够根据请求生成订阅信息,能够根据请求头部控制响应的优先级,能够存储消息一段时间。即服务器端有一定的实现成本。 Chrome 52已支持标准Web Push Protocol,其余web客户端可参考其实现,则实现成本会变低。

# 支持情况

webpush.png 在国外,Google Chrome,Firefox,以及一些厂商的系统浏览器,比如三星,都已支持Push API。预计后面会越来越多的浏览器厂商支持。 在国内,除了使用了U4内核的客户端外,还尚未有其他客户端在官方渠道宣布支持Push API,未来情况也不明朗,主要门槛在于push service的缺失。

# 总结

从上面我们可以看到,Push API提供了一种页面服务器与页面交互的方法,是web page能成长为web app所需的关键能力。同时,我们也可看到,国内是不能使用GCM/FCM作为push service的,原因众所周知。如果自己搭建push service,服务器的软硬件都需要不小的成本,而push service所带来的回报却无法预知。另外,基于web push推送的消息如何审核也是一个必须考虑的问题。 如果Web Push一直得不到支持,对于提供内核能力的SDK来说,可以提供一套非标准的API,允许客户端(比如基于U4内核的一款APP)推送消息给内核的ServiceWorker,push service则由客户端自行实现。