Skip to content

2-1.文档及链接

jake archibald

Service worker 生命周期

Using ServiceWorker Today

ServiceWorker First Draft

Offline Cookbook

w3 文档

youyuxi

register-service-worker

@vue/cli-plugin-pwa

调试

chrome://serviceworker-internals/

chrome://inspect/#service-workers

2-2.缓存策略

Cache strategies

1.CacheOnly

仅缓存是指当 Service Worker 控制着页面时,匹配的请求将只会进入缓存。

这意味着,任何缓存的资源都需要进行预缓存,以使模式正常工作,并且在 Service Worker 更新之前,绝不会在缓存中更新这些资源。

js
// Establish a cache name
const cacheName = "MyFancyCacheName_v1"

// Assets to precache
const precachedAssets = [
  "/possum1.jpg",
  "/possum2.jpg",
  "/possum3.jpg",
  "/possum4.jpg",
]

self.addEventListener("install", (event) => {
  // Precache assets on install
  event.waitUntil(
    caches.open(cacheName).then((cache) => {
      return cache.addAll(precachedAssets)
    })
  )
})

self.addEventListener("fetch", (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url)
  const isPrecachedRequest = precachedAssets.includes(url.pathname)

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(
      caches.open(cacheName).then((cache) => {
        return cache.match(event.request.url)
      })
    )
  } else {
    // Go to the network
    return
  }
})

2.NetworkOnly

仅网络是指请求通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。

这是确保内容新鲜度的好策略(比如使用标记),但需要权衡的是,用户离线时自定义设置将永远不会起作用。

3.CacheFirst

缓存优先是指:

  1. 请求到达缓存。如果资源位于缓存中,请从缓存中提供。
  2. 如果请求不在缓存中,请转到网络。
  3. 网络请求完成后,将其添加到缓存中,然后从网络返回响应。

js
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone())

          // Return the network response
          return fetchedResponse
        })
      })
    }))
  } else {
    return
  }
})

TIP

这是一项适用于所有静态资源(例如 CSSJavaScript、图片和字体)的绝佳策略,尤其是经过哈希处理的资源

它可以绕过 HTTP 缓存可能启动的服务器执行任何内容新鲜度检查,从而加快不可变资源的速度。更重要的是,所有缓存的资源都将离线可用

4.NetworkFirst

网络优先是指:

  1. 优先直接向网络请求资源,同时在缓存中保留一份。
  2. 当网络不可用,资源请求失败时,取缓存中的资源。

js
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone())

        return fetchedResponse
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url)
      })
    }))
  } else {
    return
  }
})

5.StaleWhileRevalidate

重新验证时过时是指:

  1. 在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应;
  2. 对于后续请求,首先从缓存提供资源,然后在后台从网络重新请求该资源,并更新资源的缓存条目;
  3. 对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。

js
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone())

          return networkResponse
        })

        return cachedResponse || fetchedResponse
      })
    }))
  } else {
    return
  }
})

更新策略

Give Users Control Over App Updates in Vue CLI 3 PWAs

How to Fix the Refresh Button When Using Service Workers

谨慎处理 Service Worker 的更新

1.直接使用skipWaiting

这种方式简单粗暴。

js
self.addEventListener('install', (event) => {
  self.skipWaiting()
  // 预缓存其他内容
})

✅ 优点

逻辑直接,操作简单。

❌ 缺点

  1. 较好的情况是,代码版本不一致,重新请求资源,浪费带宽。
  2. 最差的情况是,资源 404,造成解析错误。

譬如,假设我们的 serviceWorker 有两个新旧版本。

较旧的 v1 版本缓存了 /cdn/ac4e.jpg/route/about.v1.js,而更新的 v2 版本只缓存了 /route/about.v2.js,没有缓存 /cdn/ac4e.jpg

那么当内置了 skipWaitingv2 版本控制客户端后,此时 v1 版本中的 /cdn/ac4e.jpg 缓存会失效,需要重新向 CDN 上发起请求获取。

v1 版本中的懒加载路由 /route/about.v1.js 已经不存在了,会导致 404,造成解析错误。

TIP

简而言之,该策略适合始终存在的静态资源(譬如 CDN 上托管的资源),而不适合可能动态变化的资源(懒加载路由、hash 文件名资源)。

2024-05-22更新,发现上述优缺点的话述跟实际加载效果有出入(不确定是不是浏览器版本迭代过的原因):

当页面刷新时,无论何种方式,都会预取 serviceWorker 资源。如果 serviceWorker 设置了 skipWaiting,那么后续资源的加载就会在新 serviceWorker 中获取,否则是在旧 serviceWorker 中获取。

但如果是,页面打开新 tab 时,此时页面也不会及时更新,会同时存在 v1v2 两个版本:

2.使用 skipWaiting + window.reload()

在上一节中,提到了单独直接调用 skipWaiting 时,虽然 serviceWorker 会立即更新,但由于页面已经加载(譬如 index.html 已加载),因此整个页面逻辑是旧链接,会有可能导致带宽浪费或者资源 404

因此顺着这个思路来看,我们可以在 skipWaiting 后,再调用 window.reload(),强制刷新页面,从而实现整个页面的重新更新。

我们可以在 Main thread 中监听 controllerchange 事件:

js
navigator.serviceWorker.addEventListener('controllerchange', () => {
  window.location.reload()
})

当上述逻辑涉及到 Chrome Dev ToolsUpdate on Reload 功能时,为了防止无限刷新,可以额外添加一个 flag 变量:

js
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) {
    return
  }
  refreshing = true
  window.location.reload()
})

✅ 优点

相对后续方式来说,这种方式依然比较清晰简单。

❌ 缺点

无缘无故的刷新客户端页面,不利于用户体验。

TIP

不推荐使用这种方式。生产环境应用性需谨慎。

3.用户自主控制更新

本节以 vue-cli@4 为例,完整版 demo 可参考pwa-app-updates

  1. service-worker.js 文件中,预留 message 事件监听,用于监听来自 Main thread 的消息:
js
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})
  1. registerServiceWorker.js 中,注册回调函数:
js
{
  // e.g. hourly checks
  registered (registration) {
    setInterval(() => {
      registration.update()
    }, 1000 * 60 * 60) 
  },
  updated (registration) {
    document.dispatchEvent(
      new CustomEvent('swUpdated', { detail: registration })
    )
  }
}
  1. 添加通知组件 DOM
html
<template>
  <button v-if="updateExists" @click="refreshApp">
    New version available! Click to update
  </button>
</template>
  1. 在组件中监听 swUpdated 事件:
js
export default {
  data() {
    return {
      refreshing: false,
      registration: null,
      updateExists: false,
    }
  },
  created () {
    document.addEventListener(
      'swUpdated', this.showRefreshUI, { once: true }
    )
    if (navigator.serviceWorker) {  
      navigator.serviceWorker.addEventListener('controllerchange', () => {
          if (this.refreshing) return
          this.refreshing = true
          window.location.reload()
        }
      )
    }
  },
  methods: {
    showRefreshUI (e) {
      this.registration = e.detail
      this.updateExists = true
    },
    refreshApp () {
      this.updateExists = false
      if (!this.registration || !this.registration.waiting) { return }
      this.registration.waiting.postMessage({
        type: 'SKIP_WAITING'
      })
    }
  }
}

✅ 优点

该方案相对成熟,也是目前大部分 PWA 应用采用的更新策略。

  • serviceWorker 检测到资源更新时,在客户端安装完毕后,会等待激活。那么此时会弹出更新按钮供用户点击;
  • 当用户点击后,会尝试激活新的 serviceWorker
  • 当新的 serviceWorker 激活后,会利用 window.location.reload() 刷新页面,从而实现当前页面的更新。

❌ 缺点

  • 很繁琐,涉及到的文件众多,需要聚合逻辑。
  • 页面上添加了额外组件,对于生产项目来说,会影响 UI 样式。而且需要用户点击,增加了部分心智负担。

4.单个导航刷新

当客户端只存在单个 tab 时,此时刷新该 tab,即刻更新 serviceWorker,并且为了保证资源的更新,重载页面。

核心代码如下:

js
// service-worker.js
self.addEventListener('fetch', event => {
  event.respondWith((async () => {
    if (event.request.mode === 'navigate' &&
      event.request.method === 'GET' &&
      self.registration.waiting &&
      (await clients.matchAll()).length < 2
    ) {
      self.registration.waiting.postMessage({
        type: 'SKIP_WAITING'
      })
      return new Response('', {
        headers: {
          'Refresh': '0'
        }
      })
    }
    return await caches.match(event.request) ||
      fetch(event.request)
  })())
})

其中:

  1. event.request.mode === 'navigate' 表示当前请求是 navigate 请求,即浏览器导航;
  2. (await clients.matchAll()).length < 2 表示单个 tab,(仅限单个 tab 导航刷新时);
  3. new Response('', { headers: { 'Refresh': '0' } }) 表示页面导航后,重新定向刷新(延迟 0 秒)。

TIP

该条更新策略,可根据实际业务场景,选择性与第 3 条更新策略结合使用。

✅ 优点

  • 对用户来说,侵入性较弱,体验较好。

❌ 缺点

  • 出现短暂白屏,Refresh: '0' 重定向导致;
  • 浏览器兼容性较差。(截止到 2024/05/22firefox@125.0.3 版本执行依然失败)。