2024年了,是谁还在学 Service Worker?

1,457 阅读8分钟

前言

最近给平台做性能优化,恶补了一波 Service Worker,发现其中的想象空间远不止性能优化缓存这些,如果想做一些“特殊”的功能,使用 Service Work 这个独立线程可以给浏览器端很大的能力加成!

不过 Service Worker 的上手成本真的不低,如果有人问你 Service Worker 和 Web Worker 有什么关系?Service Worker 和浏览器插件里的 Service Worker(background.js) 是一回事吗?估计绝大部分同学都要蒙圈了,别着急!

本文将带领大家详细了解一下 Service Worker 到底可以做哪些事情,以及相关关联概念的异同对比

Service Worker 是什么?

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

——官方文档

用人话帮大伙理解一下这一大段:

  • Service Worker 就是一个当中介的:

    • 当用户没网的时候,这个中介也会拿之前缓存的接口数据先塞给用户凑活凑活用,用户体验这不比直接 404 好多了~
    • 这个中介可以帮忙拦截用户请求,比如每次接口访问都走缓存同时异步调用接口更新缓存(保证下一次用户使用的是最新的数据)
    • 同时他还能自己发请求和服务器交互,比如当收到新邮件给用户推送通知提示之类的

注意点

  • 只能在 HTTPS 的应用搭配使用 Service Worker(由于 Service Worker 可以拦截和处理网络请求,具有较高的权限,确保它只在 HTTPS 下运行,可以最大程度上防止恶意行为和安全漏洞)
  • Service Worker 运行在一个独立的 worker 上下文中,这意味着它和主线程(也就是运行网页和操作 DOM 的线程)是分开的。由于这种分离,Service Worker 无法直接访问 DOM 和 window 实例
  • 通过事件通讯是 Service Worker 和主应用交互的重要途径(例如使用 postMessage 方法)

极简化 demo

HTML 文件(index.html)

  • 在页面加载时,注册 Service Worker。
  • 如果 navigator.serviceWorker 存在,说明浏览器支持 Service Worker。

浏览器兼容性 👆

  • 使用 navigator.serviceWorker.register('/service-worker.js') 注册 Service Worker,并处理注册成功和失败的情况。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Service Worker Demo</title>
</head>
<body>
    <h1>Service Worker Demo</h1>
    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js').then(registration => {
                    console.log('Service Worker registered with scope:', registration.scope);
                }).catch(error => {
                    console.log('Service Worker registration failed:', error);
                });
            });
        }
    </script>
</body>
</html>

Service Worker 文件(service-worker.js)

  • 安装事件 ( install ): 打开一个缓存并将指定的资源(如 '/' 和 '/index.html')添加到缓存中。
  • 获取事件 ( fetch ): 拦截网络请求,先检查缓存中是否有匹配的资源。如果有,返回缓存的资源;如果没有,则继续网络请求。
const CACHE_NAME = 'my-cache-v1';
const urlsToCache = [
    '/',
    '/index.html'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                return cache.addAll(urlsToCache);
            })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                return response || fetch(event.request);
            })
    );
});
  • event.waitUntil 方法用于确保 Service Worker 不会在事件完成之前终止。install 事件中,通常会使用 event.waitUntil 来等待缓存操作完成,当传入 Promise 时,只有流转到 resloved 状态才能算 install 成功

  • event.respondWith 阻止浏览器的默认 fetch 操作,并且由你自己提供一个响应(可以是一个 promise)。

看起来是不是很简单!妈妈再也不用担心看不懂 ****Service Worker 逻辑了!

👍 好的,现在请手写实现以下缓存策略吧:

  1. 有缓存时返回缓存,没有时请求并缓存
  2. 只读取缓存,没有则失败
  3. 先正常发起网络请求,超时则返回缓存
  4. 不使用缓存,全部正常发起网络请求
  5. 有缓存则返回缓存,并更新内容供下次使用

什么?!!写不了一点?

写不出来别急,看下面 👇

Workbox 百宝箱

Workbox 是一组可简化常见 Service Worker 路由和缓存的模块。每个可用模块都适用于 Service Worker 开发的一个具体方面。Workbox 旨在尽可能简化 Service Worker 的使用,同时根据需要灵活地满足复杂的应用要求。

——官方文档

其中:

  • workbox-routing,用于请求匹配(比如只有成功匹配的路由才能使用缓存)

  • workbox-strategies,适用于缓存策略。

    • 上文提到的策略其实在 workbox-strategies 里都可以找到,你甚至可以自定义其他的缓存策略
  • workbox-precaching,用于预缓存。

    • 安装 Service Worker 时将一组文件保存到缓存。这通常称为“预缓存”,因为这是使用的 Service Worker 之前获取的缓存内容。
  • workbox-expiration,用于管理缓存。

    • 限制缓存中的条目数并 / 或移除长期缓存的条目。
  • workbox-window,用于注册 Service Worker 并在 window context 中处理更新。

    • 可以简化 Service Worker 的一些使用,还有监听 Service Worker 的生命周期等功能

总而言之,使用 Service Worker 你不需要自己写这些底层逻辑,用好 workbox 就够应付绝大多数环境了

Service Worker & Web Worker 傻傻分不清楚

不少小伙伴应该都使用过 Web Worker 吧?那这俩概念都带个 Worker,它们是一回事吗?

相同点

  • 它们在浏览器线程池中运行,独立于主线程。
  • 共享相同的进程环境(即浏览器实例),但各自有独立的执行上下文。
  • 通过 postMessageonmessage 事件进行通信。

这种设计的好处是能够执行耗时的操作而不阻塞主线程(通常是 UI 线程),提高应用的响应速度和用户体验。

不同点

  • 可解决问题:

    • 用户A:你这个页面操作起来太卡了,交互点击不流畅!(解决方案:使用 Web Worker 用于将高计算量任务和复杂操作移出主线程,以减少用户界面的卡顿和提高性能)
    • 用户B:你这个页面首次加载太慢了,loading 要转好久! (解决方案:使用 Service Worker 用于控制网络请求、缓存资源,并提供离线功能)
  • 生命周期

    • Web Worker 由 JavaScript 调用自行创建和销毁。每次加载页面时需重新启动
    • Service Worker 独立于网页,安装一次后,即便页面关闭也能保持活跃,通过特定事件(安装、激活、fetch)来管理,更长的生命周期也给 ****Service Worker 带来了更强大的能力

Chrome Extension 的 Service Worker 又是个什么东西?

浏览器插件开发不是本文的主要话题,所以并不会展开介绍,感兴趣的话请阅读 ➡️ 有手就行,从零开始的V3版本Chrome插件开发攻略 ,Service Worker 是 Chrome 的 Manifest V3 引入的,旨在提高性能和安全性。

首先结论:他俩主要就是重名了,并且都是脚本,除此以外没有特别大的关联!

相同点

  • 都不能直接访问 dom,毕竟这不是脚本该关心的事情
  • 都独立于页面主线程运行,不会阻塞主线程,从而提高应用性能

不同点

  • Chrome Extension Service Worker

    • 主要用于 Chrome 浏览器扩展的后台任务处理
    • 例如:管理扩展的生命周期、处理用户交互、拦截并修改网络请求、与其他扩展组件(如内容脚本、弹出页面等)通信
  • Service Worker

    • 主要用于普通 Web 应用,实现离线支持和资源缓存,增强应用的可靠性和性能
    • 例如:缓存静态资源、提供离线访问、拦截和处理网络请求

我能用 sw 做什么?

  • 请求拦截与缓存管理

    • 前文都在讨论这个,相信看到这的朋友已经很了解了
  • 推送通知

    • 比如你的平台有重要版本更新/特价广告/新闻推送/新消息,你需要及时推送通知给用户
  • 后台同步

    • 在表单提交时,用户可能会遇到网络连接问题,导致数据无法及时提交。后台同步可以在用户重新连接到网络时自动提交这些数据
  • 离线支持

    • 开发 PWA 肯定能用到,比如视频应用在离线情况下也允许用户查看已经缓存的视频

上手注意事项

  • 调试是个大坑,本文不展开叙述了,如果准备进入开发请在这方面补足功课
  • 直接卸载 Service Worker 并不会主动帮你清除缓存,你必须在卸载前显式清除缓存,否则下一次安装的 Service Worker 还会用上次的缓存
  • 缓存能玩明白才能提升用户体验,否则就是负优化。或者需要考察本平台用户是否可以接受一定程度的延迟更新带来更快的 LCP

总结

本文没有带大家一行一行写 Service Worker,而是努力带大家建立起 Service Worker 的知识框架,毕竟代码这种东西,写不完也教不完的(笑)。而且官方文档有很多例子来供大家实践学习,写技术文档如果只是搬运官方文档也太无趣了,脑子里面有概念了再进行知识填充实践起来会很快!