Skip to content

特点

  • 不能操作 / 访问 DOM

  • 会自动休眠,不会随浏览器关闭而失效,必须手动卸载

  • 离线缓存内容开发者可控

  • 必须在 https 或者 localhost 下使用

  • 所有 api 都基于 promise

缓存策略

  • cachefirst

    缓存优先

  • cacheonly

    仅缓存

  • networkfirst

    网络优先

  • networkonly

    仅网络

  • StaleWhileRevalidate

    从缓存取,用网络数据更新缓存。

实现

缓存数组中保存着需要缓存的请求资源路径,service worker 安装时,通过 const c = await caches.open(<name>) 创建一个缓存空间,然后 c.addAll(chcheList) 缓存这些请求资源路径返回的内容。通过 e.waitUntil(onCache()) 等待这个 promise 完成, 完成后再通过 e.waitUntil(skipWaiting()) 跳过等待。

service worker 激活时,service worker 并不是立即生效的,而是下一次访问才生效,我们 通过 e.waitUntil(Promise.all([clearCache(), clients.claim()])) 让 service worker 立即生效,同时清除之前的缓存。clearCache 中通过 await caches.keys(<name>) 获取所有 缓存空间的名字,遍历它们,将名字和当前名字不相等的通过 await caches.delete(key) 删除。

对请求进行拦截,如果请求的是数据会变化的接口,直接通过 fetch 发请求获取最新的数据,获取 到后克隆一份保存进行缓存中,然后将结果传入 e.respondWith 中。如果请求失败进入 catch, catch 中根据 e.request 从缓存中读取数据。

js
// 为了性能,在资源加载完毕后注册。
window.addEventListener('load', async () => {
  if (navigator.serviceWorker) {
    try {
      // 每次 sw.js 变化都会重新注册一个 service worker
      const registration = await navigator.serviceWorker.register('/sw.js')
    } catch (error) {
      console.log(error)
    }
  }
})
js
// sw,js
const version = '0' // 缓存名
const cacheList = [ // 缓存列表
  '/',
  '/api/list',
  '/index.css',
  '/manifest.json',
  '/logo.png'
]

// service worker 安装时创建缓存
const onCache = async () => {
  // 创建一个缓存空间
  const c = await caches.open(version)
  // 缓存缓存列表的数据
  await c.addAll(cacheList)
  // 跳过等待
  await self.skipWaiting()
}

// service worker 激活时清除之前的缓存
const clearCache = async () => {
  // 获取缓存空间内的所有名字
  const keys = await caches.keys(version)

  for (let i = 0, l = keys.length; i < l; i++) {
    // 删除之前的缓存
    const key = keys[i]
    if (key !== version) {
      await caches.delete(key)
    }
  }
}

// 对于数据会变化的 API,先获取最新的再缓存。
const fetchAndSave = async (request) => {
  const res = await fetch(request)
  const cloned = res.clone() // 拷贝一份用于消费
  const c = await caches.open(version)
  // 更新
  await c.put(request, cloned)
  
  return res
}

// 拦截请求
self.addEventListener('fetch', e => {
  const url = new URL(e.request.url)

  // 静态资源,不做拦截。
  if (url.origin !== self.origin) {
    return
  }

  // 如果接口数据是变化的,将数据更新到缓存中。
  if (e.request.url.includes('/api')) {
    e.respondWith(fetchAndSave(e.request).catch(async () => {
      const c = await caches.open(version)
      return c.match(e.request)
    }))
  } else {
    // service worker 不支持 ajax,因为线程中,不能发 ajax,支持 fetch。
    // respondWith 表示用什么内容返回当前响应
    e.respondWith(fetch(e.request).catch(async () => {
      // 断网,去缓存中找。
      const c = await caches.open(version)
      return c.match(e.request)
    }))
  }
})

// service worker 安装时,跳过等待。
self.addEventListener('install', e => {
  e.waitUntil(onCache())
  e.waitUntil(skipWaiting()) // 等待 promise 执行完成
})

// // service worker 不是立即生效,下一次访问时才生效。
self.addEventListener('activate', e => {
  // 激活后立刻让 service worker 生效。
  e.waitUntil(Promise.all([clearCache(), clients.claim()])) 
})