Skip to content

PWA入门之路

Published:

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

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

什么是PWA

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

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

本文主要讲一下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在网络请求上的差异:

K5afSs

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的相关信息。

G3Wse2

介绍变量caches常用方法

Cache对象常用方法

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

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

eJ2egl

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

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

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

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

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

nZJkrx

好了,接下来继续我们的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看到请求资源的信息。

lWbyAl

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

2.4. Service Worker其他功能

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

参考资料