[HTML5 API] PWA: 渐进式 Web 应用

说明

虽然我将这篇文章归到 HTML5 API 这一分类中,但确切的说 PWA 并不是单一API,而是对多种API的综合应用。特别的,一个完整的 PWA 需要两个特别的文件——服务线程(一般为 service-worker.js )和Web应用清单(一般为 manifest.jsonmanifest.webmanifest ),本文将主要对这两个文件展开介绍。

服务线程(service worker)

服务线程(service worker),是独立与页面的 WebWorker,其在启动后,会一直在后台运营(直到浏览器被关闭,或在一段时间内,没有该应用打开的的页面),并且可以拦截 Web 请求,将本地缓存甚至是即时生成的数据作为相应返回给原页面。

服务线程上下文环境

因为所有页面的请求可能都要经过服务线程,如果服务线程一但被堵塞,将会导致所有页面的请求都被堵塞,所以服务线程中的所有API均为异步API。

服务线程上下文环境是ServiceWorkerGlobalScope的实例,除了ECMAScript定义的语法级对象外,主要还有以下几个属性:

  • self: 指上下文环境本身,类似普通浏览器环境下的 window 对象,继承自WorkerGlobalScope
  • cachesCache API,同普通浏览器环境下的 caches 对象,继承自WorkerGlobalScope
  • fetch: 用于网络请求的函数,在服务线程中不支持XMLHttpRequest,请用fetch方法函数,继承自WorkerGlobalScope,但进行了重载
  • clients: 其有打开窗口及获取已有窗口等的异步方法
  • registration: 当前服务线程的状态等
  • skipWaiting: 在服务线程脚本更新后,用于强制重起服务线程

另外有与windows中同名同用法(或功能相似)的对象,均继承自WorkerGlobalScope,具体见下表:

对象 - - -
btoa location indexedDB addEventListener
atob setTimeout navigator removeEventListener
crypto setInterval performance dispatchEvent
fonts clearTimeout importScripts createImageBitmap
origin clearInterval queueMicrotask

但因为上下文环境的不通,部分API不支持或缺少部分属性成员,如navigator没有keyboardclipboardcookieEnabled等成员。

事件

ServiceWorkerGlobalScope 主要有以下几个事件:

  • install: 服务线程安装事件
  • activate: 服务线程激活事件
  • fetch: 页面或其他线程发起网络请求时的事件
  • pushsubscriptionchange: 推送订阅改变事件,属 Push API
  • push: 推送事件,属 Push API
  • message: 消息事件,postMessage 触发的事件
  • notificationclick: 通知点击事件,属 Notification API
  • notificationclose: 通知关闭事件,属 Notification API

caches

ServiceWorkerGlobalScope.cachesCacheStorage 的实例,主要有以下几个方法:

  • match(request, options): 返回所有缓存库中第一个与请求匹配的Response,返回值为Promise<Response>
  • open(name): 打开一个缓存库,返回值为Promise<Cache>
  • delete(name): 删除一个缓存库,返回值为Promise<boolean>
  • has(name): 判断是否存在指定的缓存库,返回值为Promise<boolean>
  • keys(): 获取所有的缓存库名称,返回值为Promise<string[]>

Cache 实例

通过 ServiceWorkerGlobalScope.caches.open 得到一个 Cache实例,所有的缓存操作均在此对象上实现。其实例主要有以下几个方法:

  • match(request, options): 返回缓存库中第一个与请求匹配的Response,返回值为Promise<Response>
  • matchAll(request, options): 返回缓存库中所有与请求匹配的Response,返回值为Promise<Response[]>
  • add(request): 将请求及对应的Response加入到缓存库中,Response将通过自动fetch获得
  • addAll(requests): 将多个请求及对应的Response加入到缓存库中,Response将通过自动fetch获得
  • put(request, response): 将指定的请求及对应相应关联后加入到缓存库中
  • delete(request, options): 删除与请求匹配的缓存

install 事件与 activate 事件

由于事件是通过继承EventTarget类实现的,所以,使用传统的方式,均会忽略事件回调函数返回的Promise对象,但在服务线程的安装、激活过程中,可能需要一些异步的操作,例如提前缓存部分页面。为确保触发installactivate 事件后,能够保证相关操作完成后再结束这两种状态,install 事件与 activate 事件提供了 waitUntil 方法。

waitUntil 方法接收一个 Promise 对象作为参数,当该 Promise 对象状态转为 fulfilledrejected后,installactivate状态才会结束,单如果不调用waitUntil 方法则直接结束。

fetch 事件

fetch 事件中可以劫持其他页面祸线程发起的网络请求,返回一个自定义的相应。
如果需要返回自定义请求,需要劫持原有请求,需要调用fetch 事件的 respondWith 方法。

respondWith 方法接收一个 Promise<Response> 对象作为参数,该Response对象将作为原请求的Response

Response对象,可以是在fetch 事件中调用fetch方法得到的Response,也可以是caches中缓存的Response,甚至是可以通过调用Response构造方法创建的Response对象。

其他说明

目前,服务线程只能采用cachesindexedDB两种存储方式。

Web应用清单 (Web App Manifest)

Web应用清单主要提供有关应用程序的信息(例如名称、图标和描述)。本质为JSON文件,其MIME建议为application/manifest+json

其主要有以下几个属性:

  • name: 必须,应用的名称
  • short_name: 应用的简称,一般用于主屏幕名称
  • start_url: 必须,应用的启动地址
  • display: 应用的像是方式, 目前支持:
    • browser: 按照常规网页打开,默认值
    • minimal-ui: 只保留控制导航的UI元素
    • standalone: 按照普通应用的形式显示
    • fullscreen: 全屏显示
  • background_color: 浏览器自动生产的启动页的背景色,为 CSS颜色值
  • description: 应用的描述
  • icons: 必须,应用的图标,用于桌面图标及启动页面,是一个数组,每个成员有以下几个属性:
    • src: 必须,图标的地址
    • sizes: 必须,包含空格分隔图像尺寸的字符串
    • type: 图标的mine类型
  • orientation: 显示方向,可以是以下值之一:
    • any
    • natural
    • landscape
    • landscape-primary
    • landscape-secondary
    • portrait
    • portrait-primary
    • portrait-secondary
  • scope: 定义此网站上下文的导航范围,如果超出这个范围将按照普通页面展示
  • theme_color: 网站的默认主题色

启动服务线程并加入清单文件

加入清单文件,只需要一个link标签即可,如:

<link rel="manifest" href="/manifest.json">

启动服务线程需要通过JavaScript实现:

if ('serviceWorker' in navigator) {           
  navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
    // 注册成功
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
  }).catch(function (err) {                   
    // 注册失败 :(
    console.log('ServiceWorker registration failed: ', err);
  });
}