PWA入门之路

552 阅读6分钟

由于项目中有个问题涉及到了Service Worker,所以找了时间去研究了一下PWA。也趁此写一篇文章总结一下。

前言:PWA作为今年最火热的技术概念之一,对提升Web应用的安全、性能和体验有着很大的意义,非常值得我们去了解与学习。

什么是PWA

PWA,全称Progressive Web App,即渐进式WEB应用, 是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。它的优势主要体现在:

  • 可在离线网络较差的环境下正常打开页面。
  • 安全(HTTPS)。
  • 保持最新(及时更新)。
  • 支持安装(添加到主屏幕)和消息推送
  • 向下兼容,在不支持相关技术的浏览器中仍可正常访问。

PWA本身其实是一个概念集合,它不是指某一项技术,而是通过一系列的Web技术与Web标准来优化Web App的安全、性能和体验。其中涉及到的一些技术概念包括但不限于:

  • Web App Manifest
  • Service Worker
  • Cache API 缓存
  • Push&Notification 推送与通知
  • Background Sync 后台同步

本文主要讲一下Service Worker相关的东西。

Service Worker

1. 什么是Service Worker?

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

Service worker运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage)不能在service worker中使用。

下图展示普通Web App与添加了Service Worker的Web App在网络请求上的差异:

img

2. 使用Service Worker

2.1. 注册Service Worker

在index.js文件里面注册Service Worker。

// index.js
// 注册service worker,service worker脚本文件为sw.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(function () {
        console.log('Service Worker 注册成功');
    });
}

值得一提的是,Service Worker里的各类操作都被设计为异步,以避免一些长时间的阻塞操作。这些异步操作都是建立在Promise的基础上的,如果你对Promise不够了解,建议去熟悉一下Promise。传送门:Promise(ES6标准入门)

2.2. 使用Service Worker

Service Worker的生命周期

当我们注册了Service Worker后,它会经历生命周期的各个阶段,同时会触发相应的事件。整个生命周期包括了:installing --> installed --> activating --> activated --> redundant。当Service Worker安装(installed)完毕后,会触发install事件;而激活(activated)后,则会触发activate事件。

下面的例子监听了install事件

// 在sw.js里面
// 监听install事件
self.addEventListener('install', function (e) {
    console.log('Service Worker installed');
});

self是Service Worker中的一个特殊的全局变量,类似于windowself指向当前这个Service Worker。

缓存静态资源

一般情况下,我们会列出一个需要缓存的资源列表,当Service Worker install时,会将改列表的资源缓存下来。

// sw.js
var cacheName = 'v1';
var cacheFiles = [
    '/',
    './index.html',
    './index.js',
    './index.css'
];

// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', e => {
    console.log(e);
    e.waitUntil(
        caches.open(cacheStorageKey)
        .then(cache => cache.addAll(cacheList))
        .then(_ => self.skipWaiting()) // 该函数可使新的sw.js马上生效。
    );
})

看完这段代码,你可能会有所疑惑。caches是个什么鬼东西?

caches是暴露在window作用域的一个变量,我们通过caches属性访问CacheStorage

CacheStorage是一种新的本地存储,它的存储结构是这样的:

每个域有若干个存储模块,每个模块内可以存储若干个键值对。 它的键是网络请求(Request),值是请求对应的响应(Response)。 CacheStorage的接口集中在全局变量caches中,且仅在HTTPS协议(或localhost:*域)下可用。

我们在chrome上的devtool-application中可以看到CacheStorage的相关信息。

image-20190106172427647.png

介绍变量caches常用方法

  • open(cacheName)

    返回一个 Promise,resolve为匹配 cacheName (如果不存在则创建一个新的cache)的 Cache对象。

  • keys()

    返回一个 Promise ,它将使用一个包含与 CacheStorage 追踪的所有命名 Cache对象对应字符串的数组来resolve。 使用该方法迭代所有 Cache对象的列表。

Cache对象常用方法

  • match(request, options)

    返回一个 Promise对象,resolve的结果是跟 Cache对象匹配的第一个已经缓存的请求。

  • add(request)

    抓取这个URL,检索并把返回的response对象添加到给定的Cache对象。这在功能上等同于调用 fetch(),然后使用 Cache.put() 将response添加到cache中。

  • addAll(requests)

    抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象。

  • put(request, response)

    同时抓取一个请求及其响应,并将其添加到给定的cache。

  • keys(request, options)

    返回一个Promise对象,resolve的结果是Cache对象key值组成的数组。

更多详细介绍和方法请查阅MDN-CacheStorageMDN-Cache

看到这里你可能又会问,Request???Response???

这里跟Fetch API有着密切的关系。

Request对象,用来表示资源的请求。

Response对象,用来表示一次请求的响应数据。

详细资料传送门在这里:RequestResponse

我们打印一下这两个东西,就非常明了了。

image-20190106173445713.png

好了,接下来继续我们的Service Worker。

我们可以给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后我们就可以进行一波操作了。

// sw.js
self.addEventListener('fetch', e => {
    e.respondWith(
        caches.match(e.request).then(res => {
            return res || fetch(e.request);
        })
    )
})

这里的逻辑是这样的:

  1. 浏览器发起请求,请求各类静态资源(html/js/css/img);
  2. Service Worker拦截浏览器请求,并查询当前cache;
  3. 若存在cache则直接返回,结束;
  4. 若不存在cache,则通过fetch方法向服务端发起请求,并返回请求结果给浏览器。

最终这里就简单实现了缓存静态资源文件的目的了。

更新静态缓存资源

我们通过修改cacheName来达到更新缓存资源的目的。由于浏览器判断sw.js是否更新是通过字节方式,因此修改cacheName会重新触发install并缓存资源。此外,在activate事件中,我们需要检查cacheName是否变化,如果变化则表示有了新的缓存资源,原有缓存需要删除。

self.addEventListener('activate', e => {
    e.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cache => {
                    if (cache !== cacheStorageKey) {
                        return caches.delete(cache);
                    }
                })
            )
        })
    )
    return self.clients.claim();
})

最后我们可以在network看到请求资源的信息。

image-20190106175113184.png

资源来自于Service Worker,时间也是在10ms左右,可以说是非常快的加载速度了,这样的体验对用户非常友好。

2.4. Service Worker其他功能

除了缓存静态资源文件以外,Service Worker还有缓存API数据,进行消息提醒,后台同步的功能。东西很多,目前还在慢慢探索当中。

参考资料