背景

Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。这是原生APP 本来就支持的功能,这也是相比于 web app,原生 app 更受青睐的主要原因。

基本架构

通常遵循以下基本步骤来使用 service workers:

  1. service worker URL 通过 serviceWorkerContainer.register() 来获取和注册。
  2. 如果注册成功,service worker 就在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊类型的 woker 上下文运行环境,与主运行线程(执行脚本)相独立,同时也没有访问 DOM 的能力。
  3. service worker 现在可以处理事件了。
  4. 受 service worker 控制的页面打开后会尝试去安装 service worker。最先发送给 service worker 的事件是安装事件(在这个事件里可以开始进行填充 IndexDB和缓存站点资源)。这个流程同原生 APP 或者 Firefox OS APP 是一样的 — 让所有资源可离线访问。
  5. 当 oninstall 事件的处理程序执行完毕后,可以认为 service worker 安装完成了。
  6. 下一步是激活。当 service worker 安装完成后,会接收到一个激活事件(activate event)。 onactivate 主要用途是清理先前版本的service worker 脚本中使用的资源。
  7. Service Worker 现在可以控制页面了,但仅是在 register()  成功后的打开的页面。也就是说,页面起始于有没有 service worker ,且在页面的接下来生命周期内维持这个状态。所以,页面不得不重新加载以让 service worker 获得完全的控制。

Service Worker生命周期

Service Worker支持的事件

一个完整的Service Worker示例

'use strict';

// 我们当前的缓存版本及其内容。
var CACHE = {
  version: 'site-version-number',
  resources: [
    '/index.html', // 缓存index.html
    '/css/', // 缓存/css文件夹中的所有内容
  ],
};

// 安装service worker,添加所有缓存条目
this.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open(CACHE.version).then(function (cache) {
      return cache.addAll(CACHE.resources);
    })
  );
});

// 处理fetch请求。如果未从缓存中获取,则尝试添加到缓存中。
this.addEventListener('fetch', function (event) {
  event.respondWith(
    caches
      .match(event.request)
      .then(function (resp) {
        return (
          resp ||
          fetch(event.request)
            .then(function (response) {
              return caches
                .open(CACHE.version)
                .then(function (cache) {
                  cache.put(event.request, response.clone()).catch(function (error) {
                    console.log('无法添加到缓存!' + error);
                  });
                  return response;
                })
                .catch(function (error) {
                  console.log('无法打开缓存!' + error);
                });
            })
            .catch(function (error) {
              console.log('资源未找到!' + error);
            })
        );
      })
      .catch(function (error) {
        console.log('缓存中未找到资源!' + error);
      })
  );
});

// 激活service worker
this.addEventListener('activate', function (event) {
  // 移除所有不在白名单中的缓存
  var cacheWhitelist = [CACHE.version];
  event.waitUntil(
    caches.keys().then(function (keyList) {
      return Promise.all(
        keyList.map(function (key) {
          if (cacheWhitelist.indexOf(key) === -1) {
            return caches.delete(key);
          }
        })
      );
    })
  );
});

Service Worker和浏览器缓存的区别

从上图可以看出,请求缓存流程如下

  1. 访问Service Worker缓存,如果存在就使用Service Worker缓存,不检测浏览器缓存
  2. 访问浏览器缓存,如果存在就使用,不访问服务器
  3. 上述缓存都没有,访问服务器获取资源

为什么Service Worker注册失败了?

你没有在 HTTPS 下运行你的程序

service worker文件的地址没有写对— 需要相对于 origin , 而不是 app 的根目录。在我们的例子例, service worker 是在 https://mdn.github.io/sw-test/sw.js,app 的根目录是 https://mdn.github.io/sw-test/。应该写成 /sw-test/sw.js 而非 /sw.js.

service worker 在不同的 origin 而不是你的app的,这是不被允许的。

也请注意:

service worker 只能抓取在 service worker scope 里从客户端发出的请求。

最大的 scope 是 service worker 所在的地址

如果你的 service worker 被激活在一个有 Service-Worker-Allowed header 的客户端,你可以为service worker 指定一个最大的 scope 的列表。

在 Firefox, Service Worker APIs 在用户在 private browsing mode 下会被隐藏而且无法使用。

workbox

workbox是一个库来方便我们使用service worker,创建自己的pwa应用,我们可以先了解一下 workbox的特点

  1. 不管你的站点是何种方式构建的,都可以为你的站点提供离线访问能力。
  2. 就算你不考虑离线能力,也能让你的站点访问速度更加快。
  3. 几乎不用考虑太多的具体实现,只用做一些配置。
  4. 简单却不失灵活,可以完全自定义相关需求(支持 Service Worker 相关的特性如 Web Push, Background sync 等)。
  5. 针对各种应用场景的多种缓存策略。

workbox使用

类似service worker,在 sw.js 文件里面注册workbox

// index.html
<script>
// Check that service workers are registered
if ('serviceWorker' in navigator) {
  // Use the window load event to keep the page load performant
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}
</script>
// sw.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

workbox缓存策略

缓存图片

workbox.routing.registerRoute(
  /\.(?:png|gif|jpg|jpeg|svg)$/,
  workbox.strategies.cacheFirst({
    cacheName: 'images',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
      }),
    ],
  }),
); 

缓存css和javascript

 workbox.routing.registerRoute(
  /\.(?:js|css)$/,
  workbox.strategies.staleWhileRevalidate(),
); 

缓存网页字体

 // Cache the Google Fonts stylesheets with a stale while revalidate strategy.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.googleapis\.com/,
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets',
  }),
);

// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
workbox.routing.registerRoute(
  /^https:\/\/fonts\.gstatic\.com/,
  workbox.strategies.cacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200],
      }),
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24 * 365,
      }),
    ],
  }),
); 

参考资源

神奇的 Workbox 3.0 让你的 Web 站点轻松做到离线可访问

Workbox 3:Service Worker 可以如此简单