使用 ServiceWorker 缓存 WebApp

简介: ServiceWorker 也是一种 worker,他的功能偏向代理或者缓存,应该算是功能最强大的一类 worker 了,PWA的实现就离不开ServiceWorker。

ServiceWorker 也是一种 worker,他的功能偏向代理或者缓存,应该算是功能最强大的一类 worker 了,PWA的实现就离不开ServiceWorker。

ServiceWorker API 大部分浏览器都已经支持了,可以看到 Can I Use 网站给出的数据如下

1682520411(1).png


注册


在使用ServiceWorker之前要先对其进行注册,通过 navigator.serviceWorker.register 来注册一个ServiceWorker

if ('serviceWorker' in navigator) {
  // 当浏览器支持ServiceWorker时才进行注册
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/service-worker.js');
  });
}
复制代码

多个页面共用一个ServiceWorker,但一个页面只能使用一个ServiceWorker,同样的ServiceWorker也要遵循同源和 HTTPS的约束。某种角度来说ServiceWorker可以完成 SharedWorker 能做的事情,but 有点大材小用。

注册方法返回一个 Promise 对象,注册成功的状态是 resolved 失败则为 rejected,按理来说一般不会失败。

ServiceWorker的生效范围是注册路径的上一级路径,比如引入的ServiceWorker路径为/app/sw.js,那么就会对/app下的页面生效,而在/dev等目录下则不会生效。

此时如果想要对其他作用域生效,可以显式地指定作用域

navigator.serviceWorker.register(
  '/app/service-worker.js',
  { scope: '/' }
)
复制代码

这样就可以在根目录下的所有页面生效。


通信


ServiceWorker 和主进程之间的通信也是基于事件驱动的,ServiceWorker支持了以下事件

1682520450(1).png

生命周期

其中 install 和 activate 是受生命周期控制的,分别会在安装和激活时触发,message 则会在接收到主进程的 message 时触发;右边 3 个时间是功能性事件,从左到右分别会在请求、后台同步、推送通知时触发。

如果新版本的 ServiceWorker 出现报错,会继续使用旧版文件

生命周期:

installing:正在安装,此时会触发 install 事件

↓               当 event.waitUntil 执行完成进入 installed;调用 self.skipWaiting 时可以跳过等待直接接管网页

installed:完成安装,此时并不会正常工作,而是进入 waiting 状态

↓              等页面刷新之后激活

activating:正在激活,此时触发 activate 事件,可以通过调用self.clients.claim来控制未受控的客户端

↓               当 event.waitUntil 执行完成进入 activated

activated:激活完成,此时可以正常监听功能性事件

redundant:被替换

  • install:ServiceWorker安装时触发,通常在这个时机缓存文件。
  • activate:ServiceWorker激活时触发,通常在这个时机做一些重置的操作,例如处理旧版本ServiceWorker的缓存。

1682520478(1).png

例如在 install 事件缓存文件

self.addEventListener('install', (e) => {
  let CACHE_NAME = 'app-1'
  let urls = ['/', '/index.js', '/style.css']
  e.waitUntil(
    caches.open(CACHE_NAME).
      then(cache => cache.addAll(urls))
  )
})
复制代码

在 activate 事件清理无用的缓存

self.addEventListener('activate', (e) => {
  let cacheWhitelist = ['v2']; // 新版本文件白名单
  e.waitUntil(
    Promise.all([
      self.clients.claim(),
      caches.keys().then(function (keyList) {
        return Promise.all(keyList.map(function (key) {
          if (cacheWhitelist.indexOf(key) === -1) {
            return caches.delete(key);
          }
        }));
      }])
  );
})
复制代码

在 ServiceWorker 和主进程之间互发消息和之前的 worker 不一样,我们需要通过 navigator.serviceWorker.controller获取ServiceWorker实例,然后调用 postMessage 发送消息

主进程向 worker 发送(install完成之前 controller 获取不到,所以可以用另一种方式来发送)

// client
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {
    navigator.serviceWorker.register('/service-worker.js').then((registration) => {
      // navigator.serviceWorker.controller.postMessage('msg from client')
      let serviceWorker
      if (registration.installing) {
        serviceWorker = registration.installing;
      } else if (registration.waiting) {
        serviceWorker = registration.waiting;
      } else if (registration.active) {
        serviceWorker = registration.active;
      }
      if (serviceWorker) {
        serviceWorker.postMessage({ data: 'test msg' })
      }
    });
  });
}
// ServiceWorker
self.addEventListener('message', (e) => {
  console.log(e.data);
})
复制代码

如果要从 worker 中向主进程中发送消息则需要借助 MessageChannel来完成

// client
function sendMessage(message) {
  return new Promise((resolve, reject) => {
    let messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = function (event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
    navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
  });
}
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {
    navigator.serviceWorker.register('/service-worker.js').then((registration) => {
      // navigator.serviceWorker.controller.postMessage('msg from client')
      let serviceWorker
      if (registration.installing) {
        serviceWorker = registration.installing;
      } else if (registration.waiting) {
        serviceWorker = registration.waiting;
      } else if (registration.active) {
        serviceWorker = registration.active;
      }
      if (serviceWorker) {
        sendMessage({ data: 'msg from client' }).then(data => {
          console.log(data);
        })
      }
    });
  });
}
// ServiceWorker
self.addEventListener('message', (e) => {
  console.log(e.data);
  e.ports[0].postMessage({
    msg: 'received msg',
    source: 'service-worker'
  })
})
复制代码

功能事件

最常用的功能莫过于监听 fetch事件来处理缓存了,主要是通过 fetch 和 cache 的配合来实现缓存或者代理等功能。

📢:fetch 会受 scope 限制,如果 scope 是/assets,则/api下的请求将不会触发 fetch 事件

self.addEventListener('fetch', (fetchEvent) => {
  if (!fetchEvent.request.url.startsWith(fetchEvent.request.referrer)) {
    // 不是本页面发出的请求不进行处理
    return
  }
  fetchEvent.respondWith(
    // 缓存中匹配当前请求
    caches.match(fetchEvent.request)
      .then(res => {
        // 存在缓存直接返回
        if (res) {
          return res
        } else {
          // 否则重新请求并缓存
          return fetch(fetchEvent.request).then((response) => {
            caches.open('app-1')
              .then(cache =>
                cache.put(fetchEvent.request, response)
              );
            return response.clone();
          })
        }
      })
  );
})
复制代码

通过 ServiceWorker 获取到缓存的响应

1682520516(1).png

此时甚至可以断开网络访问页面

1682520538(1).png

在缓存这里存在几种策略,上面代码属于缓存优先,除此之外还有网络优先、仅限缓存、仅限网络、异步缓存更新。

异步缓存更新就是从缓存返回结果的同时去网络刷新缓存,会在下一次请求时获取到最新数据。其他几个就跟字面意思一致了,通过 fetch 和 cache 相互配合即可完成。


调试

可以在 chrmoe 的chrome://serviceworker-internals/?devtools中看到ServiceWorker的相关信息

1682520567(1).png

点击 inspect 可以进入开发者工具

1682520626(1).png


相关文章
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
100 1
|
1天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
109 85
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
52 2
数据的存储--Redis缓存存储(二)
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
2月前
|
缓存 NoSQL 关系型数据库
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
本文深入探讨了Redis缓存的相关知识,包括缓存的概念、使用场景、可能出现的问题(缓存预热、缓存穿透、缓存雪崩、缓存击穿)及其解决方案。
223 0
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
1月前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
246 22
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
39 5
|
1月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
192 7