Service Worker 的简单使用

发布于: 8/17/2022 阅读大约需要5分钟

Service Worker 主要作用是用来拦截请求,可以让我们拦截并修改资源请求,精准地缓存资源。
特性:

  • 不能访问DOM和window对象
  • 运行于其他进程,不会阻塞主线程
  • 无法使用同步API (XHR,localStorage)
  • 只能HTTPS (localhost也可以)
  • 拦截资源请求

安装(注册)

navigator.serviceWorker.register('./sw.js')

对于浏览器兼容性问题可以使用 if('serviceWorker' in navigator)来进行判断

Service Worker会根据其所在目录的不同而拦截不同范围(scope)的请求, 比如

navigator.serviceWorker.register('./sw.js').then(r => {
  console.log('Service Worker registration successful with scope: ', r.scope)
  // http://localhost:8000/demo/
})

navigator.serviceWorker.register('./subpages/sw.js').then(r => {
  console.log('Service Worker registration successful with scope: ', r.scope)
  // http://localhost:8000/demo/subpages/
})

我们可以通过传入 scope参数来规定 Service Worker 的拦截范围(Service Worker文件所在目录为默认以及可指定的最大scope)

navigator.serviceWorker.register('./sw.js', {
  scope: './subpages'
})

无法指定ServiceWorker所在路径的上级路径, 比如 { scope: '/' }, 将会抛出错误 image.png

然后打开我们的浏览器控制台, 在应用程序(application)面板中选中 Service Workers 栏就可以看见我们注册成功的 Service Worker 了
image.png
Service Worker 注册激活后会在下次页面加载的时候启用, 可以使用 clients.claim() 立即启用

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
})

事件及方法

事件

我们可以使用 self.addEventListener('xxx')来为Service Worker添加事件监听

事件类型生命周期说明常见用途


生命周期
Lifecycle
installServiceWorker 安装触发操作IndexDB、缓存站点资源
activateServiceWorker 激活触发清除旧缓存
messageServiceWorker 接受信息触发


Functional
Events
fetch请求拦截请求
sync同步
push推送

生命周期

  1. 在调用 register 注册 Service Worker 之后会触发它的安装( install )事件,可以用来操作IndexDB和缓存站点资源
  2. 在Service Worker安装完毕后,就会触发激活事件( activate

1460000040032800.webp

更新

一个 Service Worker 在安装并激活后,如果它有新的版本, 则会在后台安装并进入等待模式, 不会激活(worker in waiting时序),直到已加载页面不再使用旧的worker才会激活。
self.skipWaiting()就是用来跳过该等待阶段,直接对现有worker进行更新.

我们可以通过以下方法来对ServiceWorker进行更新

  1. 手动调用 **update()** 进行更新 (逐字节匹配,需要Service Worker文件与历史文件不同)
  2. 自动更新(无操作后24小时)

我们可以监听 updatefound来知道 Service Worker 是否有更新来决定是否立即更新Worker

navigator.serviceWorker.register('./sw.js').then(function(registration) {
  registration.addEventListener('updatefound', function() {
    const installingWorker = registration.installing
    console.log('A new service worker is being installed:', installingWorker)
    const res = confirm('发现新的worker, 是否更新')
    res && registration.update().then(() => {
      console.log('worker update success!')
      location.reload()
    })
  });
}).catch(function(err) {
  console.log('ServiceWorker registration failed: ', err);
})

比如我们在看Vue等技术的官方文档的时候,右下角可能会提示”页面内容有更新”,让我们点击更新对内容进行更新。可以在加载页面的时候请求最新的 Service Worker 版本,与 localstorage 中的版本号进行对比,如果不同则弹框让用户选择更新或者自动调用更新方法进行更新。

const latestVersion = await request('xxxx')
navigator.serviceWorker.register('./sw.js').then(function(registration) {
  const v = localStorage.getItem(KEY_SERVICE_WORKER)
  if (v !==latestVersion) {
    registration.update().then(_ => {
      localStorage.setItem(KEY_SERVICE_WORKER, WorkerVersion)
    })
  }
})

使用场景

缓存站点资源文件

比如我们想要缓存index.html所引用的main.css样式文件

// 待缓存资源列表
const cacheUrls = [
  './theme/default.css'
]

const CACHE_NAME = 'theme-v1'
// 1. 监听 Service Worker 安装事件
self.addEventListener('install', e => {
  self.skipWaiting()
  console.log('main worker installed!', e)
  // 2. 确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成
  e.waitUntil(
    // 3. 指定缓存名称
    caches.open(CACHE_NAME).then(cache => cache.addAll(cacheUrls))
  )
})

// 4. 监听请求
self.addEventListener('fetch', e => {
  e.respondWith(
    // 5.使用缓存来匹配当前请求的URL
    caches.match(e.request).then(res => {
      // 6. 如果有对于缓存, 直接返回缓存, 否则请求资源
      return res || fetch(e.request)
    })
  )
})
  1. 监听 install 事件
  2. 使用 waitUntil来确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成
  3. 使用 Cache API来创建并缓存指定 url 资源
    1. caches.open(缓存名称)用来创建指定名称缓存
    2. addAll(urls)用来指定缓存资源列表
  4. 监听 fetch 事件(拦截ServiceWorker指定scope下的浏览器请求)
  5. 匹配缓存资源(通过 url 和 vary header进行)
  6. 无匹配的缓存资源, 则恢复请求

我们还可以通过配置自定义 request header 来让 Service Worker 动态缓存请求资源
(改造上面步骤6)

function handleRequest(request) {
  // 有自定义缓存头 offline: 1, 则缓存资源
  if (request.headers.get('offline') === '1') {
    console.log('cache request:', e.request.url)
    return fetch(request).then(res => {
      return caches.open(CACHE_NAME).then(cache => {
        cache.put(request, res.clone())
        return res
      })
    })
  } else {
    return fetch(request)
  }
}

const cacheUrls = [
  'theme/theme-default.css',
]

const CACHE_NAME = 'style-v2'

self.addEventListener('install', e => {
  self.skipWaiting()
  console.log('main worker installed!', e)
  e.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(cacheUrls))
  )
})

self.addEventListener('fetch', e => {
  e.respondWith(
    // 5.使用缓存来匹配当前请求的URL
    caches.match(e.request).then(res => {
      // 6. 如果有对于缓存, 直接返回缓存, 否则请求资源
      return res || handleRequest(e.request)
    })
    // 没有资源或者网络不可用的回退方案
    .cache(() => {
      return caches.match('/theme/theme-default.css')
    })
  )
})
// 注册
navigator.serviceWorker.register('./sw.js').then(function(registration) {
  console.log('ServiceWorker registration successful with scope: ', registration.scope, registration);
}).catch(function(err) {
  console.log('ServiceWorker registration failed: ', err);
});

// 添加测试按钮
const btn = document.createElement('button')
btn.innerHTML = '改变主题(缓存)'
document.body.appendChild(btn)

// 添加按钮事件
btn.addEventListener('click', () => {
  // 自定义请求头
  const headers = new Headers()
  headers.set('offline', '1')
  // 请求资源
  fetch('./theme/theme-dark.css', {
    headers
  }).then(async res => {
    const fileContent = await res.text()
    console.log('改变主题', 'dark')
    const styleElement = document.createElement('style')
    styleElement.innerHTML = fileContent
    styleElement.setAttribute('rel', 'stylesheet')
    document.head.appendChild(styleElement)
  })
})

打开devtools后,我们点击按钮进行测试,可以在网络面板看到,请求先访问了ServiceWorker,发现没有缓存后ServiceWorker进行资源请求,所以会有两次请求
image.png
我们再次点击按钮请求资源, 发现只多了一个请求,为ServiceWorker从缓存中读取
image.png
具体的缓存记录我们可以在 应用程序(application) 面板中的 缓存 菜单查看
image.png

参考链接