使用Service Worker

2,974 阅读5分钟

Service Worker 与 PWA

PWA全称为Progressive Web App,即渐进式Web应用。其理念是通过多种技术来增强Web应用体验,以提供接近原生应用的用户体验。

与PWA相关的技术点主要包含:

  • Manifest:使得Web应用可以被添加到主屏幕。需要创建一个用以描述Web应用被添加至主屏幕的名称、图标等信息的manifest.json文件。
  • Service Worker:为Web应用提供离线缓存,使得Web应用可以在离线状态下继续使用(部分功能)。
  • Push Notification:提供了消息推送功能。

作为PWA中重要的一部分,今就来简单的了解一下Service Worker。

使用Service Worker

Service Worker遵循下载、安装、激活的生命周期,并提供了fetch等事件,允许我们对资源的获取实施干预。

Service Worker的生命周期:

Service Worker的生命周期(图片来源MDN)

Service Worker的事件:

Service Worker的事件(图片来源MDN)

注册

我们通过serviceWorker.register注册我们的Service Worker:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register(</path/to/your_service_worker.js>, { scope: './' }).then((reg) => {
    //
  }).catch((error) => {
    //
  });
}

注册时我们需要提供我们的Service Worker代码的路径。scope参数是可选的,用以指定service worker可控制的内容的子目录:service worker 只能抓取在 service worker scope 里从客户端发出的请求。

单个service worker可以控制多个页面: 每个页面不会有自己独有的worker

你的Web应用需要在HTTPS下运行,同时Web应用于service worker应当在同一个origin下

安装和激活

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        ...
      ]);
    })
  );
});
  • install事件会在安装完成之后触发。
  • 我们添加了install事件的监听器,并通过event.waitUntil确保在安装完成之前添加这些缓存。
  • 我们通过caches.open创建了一个名叫v1的缓存,并通过addAll方法将目标资源加入缓存。

Cache

Cache了提供控制缓存的能力:

  • cache.add(<request|url>)cache.addAll(<url_list>):抓取目标URL, 检索并把返回的response对象添加到给定的Cache对象
  • cache.put<request>, <response>:同时抓取一个请求及其响应,并将其添加到给定的cache
  • cache.match(<request>):匹配缓存,返回空或Response对象
  • cache.delete(<key>):删除缓存对象
  • cache.keys:列出所有键
  • Cache的键为Request对象,值为Reponse对象
  • 方法都会返回Promise对象

CacheStorage提供了缓存的管理:

  • caches.open(<name>):创建一个名为name的缓存
  • caches.keys():列出所有缓存的键
  • caches.has(<name>):判断缓存是否存在
  • caches.delete(<name>):删除缓存
  • 方法都会返回Promise对象

caches API在页面和SW中都可以使用

自定义请求的响应

我们可以添加fetch事件的监听,并通过event.respondWith劫持响应:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    //
  );
});

例如,我们可以尝试先匹配缓存,若不存在,则请求网络:

event.respondWith(
  caches.match(event.request).then(function(response) {
    return response || fetch(event.request);
  })
);

资源缓存策略

安装:缓存关键资源

在安装过程中,我们可以将运行Web应用所需的静态资源(html、js、css、图片...)加入缓存,如同之前在介绍安装和激活时所作的那样。

而有些资源,并非关键的依赖项,允许缓存失败:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function(cache) {
      cache.addAll(
        // 非依赖项
      );
      return cache.addAll(
        // 关键资源
      );
    })
  );
});

非依赖项即使缓存失败,应用在离线状态下依然可用。

激活:移除旧缓存

active事件中,新版本的SW已经激活,旧版本的SW退出,此时我们可以通过caches.keyscaches.delete删除旧的缓存。

this.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.filter((cacheName) => {
          // 判断是否要移除该缓存
        }).map((cacheName) => {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

预缓存资源

caches api在页面中同样可以使用,因此我们可以想象这样一个功能:我们在点击某本书的“加入书架”按钮时,下载并缓存这本书的前几章内容,以便用户之后在离线状态下也能阅读书籍的前几章:

onAddBook({ bookId }) {
  caches.open(`mysite-books-${bookId}`).then((cache) => {
    return fetch(`/book/${bookId}/chapters`).then((response) => {
      // 缓存前十章内容
      return response.json().data.slice(0, 10).map(chapter => chapter.url)
    }).then((urls) => {
      cache.addAll(urls);
    })
  })
}

网络响应与缓存策略

仅使用缓存

event.respondWith(caches.match(event.request));

仅使用网络

event.respondWith(fetch(event.request));

缓存优先

我们可以先尝试在缓存中寻找资源,若不存在,则请求网络资源,同时在网络资源返回时放入缓存:

event.respondWith(
  caches.open('mysite-dynamic').then((cache) => {
    return cache.match(event.request).then((response) => {
      return response || fetch(event.request).then((response) => {
        cache.put(event.request, response.clone());
        return response;
      });
    });
  })
);

网络优先

event.respondWith(
  fetch(event.request).catch(() => {
    return caches.match(event.request);
  })
);

缓存优先并更新

对于以上的例子,我们可以优先使用缓存,但同时仍然发起网络请求并更新资源:

event.respondWith(
  caches.open('mysite-dynamic').then((cache) => {
    return cache.match(event.request).then((response) => {
      const fetchPromise = fetch(event.request).then((response) => {
        cache.put(event.request, response.clone());
        return response;
      });
      return response || fetchPromise;
    });
  })
);

自定义404/离线页面

我们还可以自定义离线响应:

event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    }).catch(() => caches.match('/offline.html'));
  );

需要注意的是,缓存空间在资源紧张时会被回收

库与工具

Workbox

Workbox是谷歌推出的方便大家使用PWA/SW的库。

我们可以在Service Worker库中引入Workbox并使用:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');

workbox.routing.registerRoute(
  /.*\.css/,
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'css-cache',
  })
);

在以上例子中,我们为css文件资源进行了缓存,使用的策略是之前介绍的“缓存优先并更新”。

Workbox提供了不同的缓存策略,例如:CacheFirst,NetworkFirst,StaleWhileRevalidate等等。

workbox-webpack-plugin

不仅如此,Workbox还提供了workbox-webpack-plugin方便我们在Webpack打包的项目中更方便的使用Workbox。

它提供的GenerateSW插件会在打包时生成service-worker.js,我们只需引用它就行了。

workbox-webpack-plugin,为我们完成SW相关的许多任务,包括但不限于:

  • 缓存静态资源

会为我们生成precache-manifest.<revision>.js文件,其内容形似:

self.__precacheManifest = [
  {
    "revision": "bd5a1eafb4eb894699d3",
    "url": "../a.47886103.css"
  },
  {
    "revision": "15f2587b088bb42d8017",
    "url": "../b.7e08be0e.js"
  },

生成的SW文件会加载manifest文件并使用workbox.workbox.precaching.precacheAndRoute添加缓存:

workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

// precacheAndRoute
precache(entries);
addRoute(options);

// precache
self.addEventListener('install', (event) => {
  event.waitUntil(
      precacheController.install({event, plugins})
          .catch((error) => {
            ...
          })
  );
});
  • 自动生成route注册代码

根据Webpack配置中的runtimeCaching设置,自动生成代码调用workbox.routing.registerRoute注册请求过程中的缓存策略。runtimeCaching形似:

runtimeCaching: [
  {
  	urlPattern: /api/,
  	handler: 'networkFirst',
  	cacheName: 'api-cache',
  },
  {
  	urlPattern: /*.(jpg|png|gif)$/,
  	handler: 'staleWhileRevalidate',
  	cacheName: 'image-cache',
  },
  ...
]

register-service-worker

最后,我们可以使用register-service-worker来简化SW的注册和回调钩子:

import { register } from 'register-service-worker'

register('/service-worker.js', {
  registrationOptions: { scope: './' },
  ready (registration) {},
  registered (registration) {},
  cached (registration) {},
  updatefound (registration) {},
  updated (registration) {},
  offline () {},
  error (error) {}
})

参考