前端离线缓存之 “ Service Worker ” - 掘金

前端离线缓存之 “ Service Worker ”

1,736 阅读5分钟
定义

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

特性
  1. 一个独立的worker线程,独立于当前网页进程,有自己独立的workercontext。
  2. 一旦被install,就永远存在,除非被手动unregister
  3. 用到的时候可以直接唤醒,不用的时候自动睡眠
  4. 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  5. 离线内容开发者可控
  6. 能向客户端推送消息
  7. 不能直接操作DOM
  8. 必须在HTTPS或者localhost环境下才能工作
  9. 异步实现,内部大都是通过Promise实现
生命周期
定义:简单来说,分为三个阶段:“注册(register)”、“安装(Install)”、“激活(activate)”, 还有最重要的一个拦截事件 "fetch"

service_worker_lifecycle.png

// main.js
/**
 * 特别说明
 * @scope 表示定义service worker注册范围的URL ;service worker可以控制的URL 范围
 */
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('sw.js', {scope: './'})
  .then(function(registration) {
    // do something
  }).catch(function(error) {
    // do something
  });
}
// sw.js 文件
self.addEventListener('install', function(event) {
  // do something
});
self.addEventListener('activate', function (event) {
  // do something
})


/*
     * The most important
 */
self.addEventListener('fetch', function (event) {
  /**
    1. 主要通过 CacheStorage 、Cache 等 API 操作"离线数据"和"实时数据"
    2. 返回给客户端的数据主要分为:
       1. Network First:网络优先策略
       2. Cache First:缓存优先策略
       3. Network Only:仅通过发送正常的网络请求获取资源,并将请求响应结果直接返回。
       4. Cache Only:仅从缓存中读取资源。
  
  */
})

工作流程
注意事项
  • Service Worker 文件只在首次注册的时候执行了一次
  • 安装激活流程也只是在首次执行 Service Worker 文件的时候进行了一次。
  • fetch 事件
    • 首次注册成功的 Service Worker 没能拦截当前页面的请求。
    • 非首次注册的 Service Worker 可以控制当前的页面并能拦截请求。
    • 原因:为什么首次没有拦截到网络请求呢?主要是因为在 Service Worker 的注册是一个异步的过程,在激活完成后当前页面的请求都已经发送完成,因为时机太晚,此时是拦截不到任何请求的,只能等待下次访问再进行

service_worker_process.png

Service Worker 更新原理
skipWaiting
  • Service Worker 一旦更新,需要等所有的终端都关闭之后,再重新打开页面才能激活新的 Service Worker,这个过程太复杂了。通常情况下,开发者希望当 Service Worker 一检测到更新就直接激活新的 Service Worker。

service_worker_update_process.png

Service Worker 调试
本地数据储存

经过以上的铺垫,现在可愉快操作离线数据实时数据了,先看一段代码,后补充!

const CACHE_NAME = 'fed-cache'
const Self = globalThis

Self.addEventListener('install', function (event) {
  Self.skipWaiting()
  Self.caches.open(CACHE_NAME)
})
Self.addEventListener('fetch', function (event) {
  /*
   * 是否含有网络
   * 是:进行网络请求,且更新cache数据(保持数据比较新)
   * 否:进行离线缓存
   */
  if (Self.navigator.onLine) {
    util.fetchPut(event.request.clone())
  } else {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response
      })
    )
  }
})

let util = {
  fetchPut: function (request) {
    return fetch(request).then((response) => {
      const responseClone = response.clone()
      if (util.noCache(response)) {
        return response
      }
      if (request.method === 'GET') {
        Self.caches.open(CACHE_NAME).then((cache) => {
          cache.put(request, responseClone)
        })
      }
      return response
    })
  },
  noCache: function (response) {
    if (
      !response ||
      response.status !== 200 ||
      response.type !== 'basic' ||
      !response.url.includes('http') ||
      response.url.includes('vite')
    ) {
      return true
    }
    return false
  }
}

下面将介绍操作缓存的API,我自个理解一个概念,Cache 类似IndexedDB都为数据库 CacheStorage API 主要操作 数据库 Cache API 主要操作 数据表

CacheStorage API
  1. CacheStorage.open() 创建/打开数据库
  2. CacheStorage.match()所有数据库中,检索符合条件的返回Response 对象
  3. CacheStorage.has() 是否含有某个数据库
  4. CacheStorage.delete() 删除某个数据库
  5. CacheStorage.keys() 遍历所有的数据库,返回数组(包含数据库名字)
Cache API
  1. Cache.match(request, options) 查询单条数据
  2. Cache.matchAll(request, options) 查询多条数据
  3. Cache.add(request) 添加一条数据
  4. Cache.addAll(requests) 添加多条数据
  5. Cache.put(request, response) 添加一条数据
  6. Cache.delete(request, options) 删除一条数据
  7. Cache.keys(request, options) 遍历数据表
缓存空间的使用情况

主要用于管理空间内存,达到限制要定时处理。

/**
 * 查询当前缓存空间的使用情况
 * 缓存资源的过期失效和清理工作,尽量避免被动触发浏览器的资源清理
 */
navigator.storage.estimate().then((estimate) => {
  // 设备为当前域名所分配的存储空间总大小
  console.log(estimate.quota)
  // 当前域名已经使用的存储空间大小
  console.log(estimate.usage)
})
FetchEvent
  1. FetchEvent.respondWith() 主要防止异步操作, 和 async await 感觉一样,扩展延长 fetch 事件生命周期的作用
 // 错误用法
self.addEventListener('fetch', event => {
    // 因fetch属于异步,存在fetch 代码已执行完毕,而setTimeout 未触发的可能性
    setTimeout(() => {
      event.respondWith(new Response('Hello World!'))
    }, 1000)
})
// 正确用法

// 等待 1 秒钟之后异步返回 Response 对象
event.respondWith(new Promise(resolve => {
  setTimeout(() => {
    resolve(new Response('Hello World!'))
  }, 1000)
}))
  1. ExtendableEvent.waitUntil() 方法告诉事件分发器该事件仍在进行。这个方法也可以用于检测进行的任务是否成功。在服务工作线程中,这个方法告诉浏览器事件一直进行,直至 promise 解决,浏览器不应该在事件中的异步操作完成之前终止服务工作线程。 一句话 延长生命周期, 可用于installfetch 事件
addEventListener('install', event => {
  const preCache = async () => {
    const cache = await caches.open('static-v1');
    return cache.addAll([
      '/',
      '/about/',
      '/static/styles.css'
    ]);
  };
  event.waitUntil(preCache());
});
通信 ClientsClient
 // main.js (主线程)
 navigator.serviceWorker.addEventListener('message', (event) => {
   console.log(`message: ${event.data}`)
 })
 
 // sw.js  文件
  self.clients.matchAll().then(allClients=>{
    allClients.forEach(client => {
      client.postMessage('I am Rainy')
    });
  })
  
最后注意的点
  1. Cache.put, Cache.addCache.addAll只能在GET请求下使用。
  2. ServiceWorker, 只能fetch进行拦截,axios不行
参考文章
  1. Service Worker