Workbox 之 ServiceWorker可以如此简单

码上见分晓 1775阅读 发布于2年前

如果你追求极致的WEB体验,你一定在站点中使用过PWA,也一定面临过在编写serviceWorker代码时的犹豫不决,因为serviceWorker太重要了,一旦注册在用户的浏览器,全站的请求都会被serviceWorker控制,一不留神,小问题也成了大问题了。不过到了现在有了Workbox,一切关于serviceWorker都不再是问题。

Service Worker 生命周期

生命周期

我们可以看到生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃

  • * 安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源  进行离线缓存。
  • install 事件回调中有两个方法:
  • * event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
  • * self.skipWaiting()self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。
  • 安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
  • 激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
  • activate 回调中有两个方法:
  • * event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。

  • * self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。

  • * 激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)。

  • * 废弃状态 ( redundant ):这个状态表示一个 Service Worker 的生命周期结束。


  • 这里特别说明一下,进入废弃 (redundant) 状态的原因可能为这几种:

    • * 安装 (install) 失败

    • * 激活 (activating) 失败

    • * 新版本的 Service Worker 替换了它并成为激活状态


科普ServiceWorker

如果你已经熟悉ServiceWorker,可以跳过此段。

ServiceWorker是PWA中最重要的一部分,它是一个网站安插在用户浏览器中的大脑。ServiceWorker是这样被注册在页面上的

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    // 注册一个service worker,这个例子中worker的路径是根目录中的,所以这个worker可以缓存这个项目中任意文件。如果目录是‘/js/sw.js‘,那么只能缓存目录'/js'下的文件
    // 参数registration存储了本次注册的一些相关信息
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      // registration.scope 返回的是这个service worker的作用域
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    })
  })
}

为什么说SW(下文将ServiceWorker简称为SW)是网站的大脑?举个例子,如果在www.webascii.cn的根路径下注册了一个SW,那么这个SW将可以控制所有该浏览器向www.webascii.cn站点发起的请求。只需要监听fetch事件,你就可以任意的操纵请求,可以返回从cacheStorage中读的数据,也可以通过fetch API发起新的请求,甚至可以new一个Response,返回给页面。

// 一段糟糕的sw代码,在这个SW注册好以后,整个SW控制站点的所有请求返回的都将是字符串"bad",包括页面的HTML
self.addEventListener('fetch', function(event) {
  event.respondWith(
    new Response('bad')
  );
});

就是因为SW权利太大了,写起来才会如履薄冰,一不小心有些页面资源就不能及时正确的更新了。

一个还算完整ServiceWorker

先来看一个直接手写的SW文件

var cacheStorageKey = 'cachesName'
var cacheList = [
  // 注册成功后要立即缓存的资源列表
  '/',
  'index.html',
  'index.js',
  'index.css',
  'index.png'
]

// 当浏览器解析完sw文件时,Service worker内部触发install事件
self.addEventListener('install', function(e) {
  // install事件中一般会将cacheList中要换存的内容通过addAll方法,拉一遍放入caches中
  e.waitUntil(
    // 打开一个缓存空间,将需要缓存的资源添加到缓存里面
    caches.open(cacheStorageKey).then(function(cache) {
      return cache.addAll(cacheList)
    })
  )
})
// 如果当前浏览器没有激活的Service worker或者已经激活的Service worker被解雇时
// 新的Service worker 激活触发activate事件
self.addEventListener('activate', function(e) {
  // active事件中通常做一些过期资源释放的工作,匹配到就从caches中删除
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    return Promise.all(cacheNames.map(name => {
      // 如果缓存资源的key 与 当前需要缓存的key 不同时
      if (name !== cacheStorageKey) {
        // 释放缓存
        return caches.delete(name);
      } else {
        return Promise.resolve();
      }
    }))
  })
  
  e.waitUntil(
    Promise.all([cacheDeletePromises])
  )
})

self.addEventListener('fetch', function(e) {
  // 在此编写缓存策略
  e.respondWith(
    // 可以通过匹配缓存中的资源返回
    caches.match(e.request)
    // 也可以从远端拉取
    fetch(e.request.url)
    // 也可以自己造
    new Response('自己造')
    // 也可以通过吧fetch拿到的响应通过caches.put方法放进chches
  )
})

其实所有站点SW的install和active都差不多,无非是做预缓存资源列表,更新后缓存清理的工作,逻辑不太复杂,而重点在于fetch事件。上面的代码,我把fetch事件的逻辑省略了,因为如果认真写的话,太多了,而且也不利于讲明白缓存策略这件事。想象一下,你需要根据不同文件的扩展名把不同的资源通过不同的策略缓存在caches中,各种css,js,html,图片,都需要单独搞一套缓存策略,你就知道fetch中需要写多少东西了吧。

Workbox 3

workbox的出现就是为了解决上面的问题的,它被定义为PWA相关的工具集合,其实围绕它的还有一些列工具,如workbox-cli、gulp-workbox、webpack-workbox-plagin等等,不过他们都不是今天的重点,今天想聊的就是workbox本身。

其实可以把workbox理解为Google官方PWA框架,它解决的就是用底层API写PWA太过复杂的问题。这里说的底层API,指的就是去监听SW的install, active, fetch事件做相应逻辑处理等。使用起来是这样的

// 首先引入workbox框架
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.3.0/workbox-sw.js')
// Set workbox config
workbox.setConfig({
  "modulePathPrefix": "https://static.webascii.cn/webascii/service-worker/workbox-v4.2.0",
  "debug": true
})

workbox.precaching([
  // 注册成功后要立即缓存的资源列表
])
// 一旦激活就开始控制任何现有客户机(通常是与skipWaiting配合使用)
// https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox-core_clientsClaim.mjs
workbox.core.clientsClaim()
// 跳过等待期
// https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox-core_skipWaiting.mjs
workbox.core.skipWaiting()
// 删除过期缓存
// https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox-precaching_cleanupOutdatedCaches.mjs
workbox.precaching.cleanupOutdatedCaches()

// html的缓存策略
workbox.routing.registerRoute(
  new RegExp('/'),
  new workbox.strategies.NetworkFirst ({
  // 缓存自定义名称
  cacheName:'html-caches',
  plugins: [
    // 需要缓存的状态筛选
    new workbox.cacheableResponse.Plugin({
      statuses: [0, 200]
    }),
    // 缓存时间(秒)
    new workbox.expiration.Plugin({
      // 最大缓存数量
      maxEntries: 20,
      // 缓存时间12小时
      maxAgeSeconds: 12 * 60 * 60
    })
  ]
}), 'GET')

// js css的缓存策略
workbox.routing.registerRoute(
  new RegExp('.*\.(?:js|css)'),
  workbox.strategies.cacheFirst({
      cacheName:'js-css-caches',
      plugins: [
        // 需要缓存的状态筛选
        new workbox.cacheableResponse.Plugin({
          statuses: [0, 200]
        }),
        // 缓存时间
        new workbox.expiration.Plugin({
          maxEntries: 20,
          maxAgeSeconds: 12 * 60 * 60
        })
      ]
    })
)

// CDN的缓存策略
workbox.routing.registerRoute(
  new RegExp('https://static\.webascii\.cn/.*'),
  new workbox.strategies.StaleWhileRevalidate ({
  cacheName:'cdn-static-caches',
  plugins: [
    // 需要缓存的状态筛选
    new workbox.cacheableResponse.Plugin({
      statuses: [0, 200]
    }),
    // 缓存时间
    new workbox.expiration.Plugin({
      maxEntries: 20,
      maxAgeSeconds: 12 * 60 * 60
    })
  ]
}), 'GET')

// api get接口的缓存策略
workbox.routing.registerRoute(
  new RegExp('https://new\.api\.webascii\.cn/api/.*'),
  new workbox.strategies.NetworkFirst({
  cacheName:'api-caches',
  plugins: [
    // 需要缓存的状态筛选
    new workbox.cacheableResponse.Plugin({
      statuses: [0, 200]
    }),
    // 缓存时间
    new workbox.expiration.Plugin({
      maxEntries: 20,
      maxAgeSeconds: 12 * 60 * 60
    })
  ]
}), 'GET')

上面的代码理解起来就容易的多了,通过workbox.precaching中的是install以后要塞进caches中的内容,workbox.routing.registerRoute中第一个参数是一个正则,匹配经过fetch事件的所有请求,如果匹配上了,就走相应的缓存策略workbox.strategies对象为我们提供了几种最常用的策略,如下

Stale-While-Revalidate

Cache FirstNetwork FirstNetwork Only

Cache Only

你可以通过plagin扩展这些策略,比如增加个缓存过期时间(官方有提供)什么的。甚至可以继续监听fetch事件,然后使用这些策略,官方文档在这里.

经验之谈

在经过一段时间的使用和思考以后,给出我认为最为合理,最为保守的缓存策略。

HTML,如果你想让页面离线可以访问,使用NetworkFirst,如果不需要离线访问,使用NetworkOnly,其他策略均不建议对HTML使用。

CSS和JS,情况比较复杂,因为一般站点的CSS,JS都在CDN上,SW并没有办法判断从CDN上请求下来的资源是否正确(HTTP 200),如果缓存了失败的结果,问题就大了。这种我建议使用Stale-While-Revalidate策略,既保证了页面速度,即便失败,用户刷新一下就更新了。

如果你的CSS, JS与站点在同一个域下,并且文件名中带了Hash版本号,那可以直接使用Cache First策略。

图片建议使用Cache First,并设置一定的失效事件,请求一次就不会再变动了。

上面这些只是普适性的策略,见仁见智。

还有,要牢记,对于不在同一域下的任何资源,绝对不能使用Cache only和Cache first。

参考文献

Workbox官方文档

神奇的 Workbox 3.0

分享PPT下载

Service Worker 实践.pptx


修改历史(2条记录)

登录 后发表评论
  • 评论 0
  • 9
  • Top