JS Service Workers:离线缓存、网络请求拦截与 PWA 架构

嘿,各位未来的前端大神们,欢迎来到今天的 "JS Service Workers:让你的网站离线也能浪" 讲座!我是你们今天的导游,啊不,讲师,带大家一起探索 Service Worker 这个神奇的小家伙。

准备好了吗? 那我们就发车啦!

第一站: 啥是 Service Worker?别跟我说那些官方定义!

来,抛开那些教科书式的定义,咱们用人话说说 Service Worker 是个啥。

你可以把它想象成一个驻扎在你浏览器里的小弟,专门负责处理网络请求和缓存。它独立于你的网页运行,这意味着即使你关掉了网页,它还在后台默默地工作。 这就厉害了!

  • 特点一:独立自主,偷偷摸摸干活: 它不依赖于你的页面,只要浏览器没关,它就一直候着,随时准备接管网络请求。
  • 特点二:中央调度,统一管理: 所有的网络请求都得经过它,它有权决定是直接从缓存里拿,还是去网络上请求。
  • 特点三:默默守护,离线救星: 即使网络断了,只要它缓存了资源,你的网站依然可以正常显示。

第二站:Service Worker 的一生:注册、安装、激活

Service Worker 的生命周期就像一个人的成长过程,要经历注册、安装、激活三个阶段。

  • 注册 (Registration): 告诉浏览器 "嘿,这里有个 Service Worker,你管一下"。

    // main.js (你的主 JavaScript 文件)
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js') // 注意路径
        .then(registration => {
          console.log('Service Worker registered with scope:', registration.scope);
        })
        .catch(error => {
          console.log('Service Worker registration failed:', error);
        });
    }

    解释一下:

    • 'serviceWorker' in navigator: 检查浏览器是否支持 Service Worker。
    • navigator.serviceWorker.register('/sw.js'): 注册 Service Worker,/sw.js 是 Service Worker 文件的路径。 注意这个路径是相对于你的页面而言的。
    • .then(...).catch(...): 处理注册成功和失败的情况。
  • 安装 (Installation): Service Worker 接收到注册成功的信号后,开始安装。这是一个一次性的过程,通常用来缓存静态资源。

    // sw.js (你的 Service Worker 文件)
    const CACHE_NAME = 'my-site-cache-v1';
    const urlsToCache = [
      '/',
      '/index.html',
      '/style.css',
      '/script.js',
      '/images/logo.png' // 确保路径正确
    ];
    
    self.addEventListener('install', event => {
      // Perform install steps
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(cache => {
            console.log('Opened cache');
            return cache.addAll(urlsToCache);
          })
      );
    });

    解读一下:

    • CACHE_NAME: 缓存的名字,每次更新缓存都要修改这个名字,强制浏览器更新缓存。
    • urlsToCache: 需要缓存的资源列表。 确保路径正确。
    • self.addEventListener('install', ...): 监听 install 事件,当 Service Worker 开始安装时触发。
    • event.waitUntil(...): 告诉浏览器,在完成 waitUntil 里的操作之前,不要认为安装完成。
    • caches.open(CACHE_NAME): 打开一个名为 CACHE_NAME 的缓存。
    • cache.addAll(urlsToCache): 将 urlsToCache 里的所有资源添加到缓存中。
  • 激活 (Activation): 安装完成后,Service Worker 进入激活状态。这是清理旧缓存的好时机。

    self.addEventListener('activate', event => {
      const cacheWhitelist = [CACHE_NAME]; // 白名单,只保留这些缓存
    
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (cacheWhitelist.indexOf(cacheName) === -1) {
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });

    解释一下:

    • cacheWhitelist: 白名单,只有白名单里的缓存才会被保留。
    • caches.keys(): 获取所有缓存的名字。
    • cacheNames.map(...): 遍历所有缓存的名字,如果不在白名单里,就删除它。

第三站:拦截网络请求:Service Worker 的核心技能

Service Worker 最重要的功能就是拦截网络请求,并决定是从缓存里拿,还是去网络上请求。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // IMPORTANT: Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the response.
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          response => {
            // Check if we received a valid response
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as cache it we need to clone it so we have two.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
  );
});

解读一下:

  • self.addEventListener('fetch', ...): 监听 fetch 事件,当浏览器发起网络请求时触发。
  • event.respondWith(...): 拦截请求,并返回一个 Promise,这个 Promise 的结果就是浏览器最终得到的响应。
  • caches.match(event.request): 在缓存中查找与请求匹配的资源。
  • response: 如果找到了,就直接返回缓存中的响应。
  • fetch(event.request.clone()): 如果没找到,就发起网络请求。 务必使用clone,因为request和response 都是stream,只能被消费一次。
  • cache.put(event.request, responseToCache): 将网络请求的响应添加到缓存中。

第四站:缓存策略:选择适合你的套路

不同的网站有不同的需求,选择合适的缓存策略非常重要。 这里介绍几种常见的缓存策略:

缓存策略 描述 优点 缺点 适用场景
Cache First 优先从缓存中获取资源,如果缓存中没有,再去网络请求,并将结果缓存起来。 速度快,离线可用 缓存未更新时,可能导致显示旧版本 静态资源(如 CSS、JavaScript、图片),不经常更新的资源
Network First 优先从网络请求资源,如果网络请求失败,再去缓存中获取。 保证获取最新的资源,网络恢复后自动更新 速度慢,离线不可用 经常更新的资源,对实时性要求高的资源
Cache Only 只从缓存中获取资源,如果缓存中没有,就返回错误。 速度快,完全离线可用 如果缓存中没有资源,则无法显示 静态资源,在离线场景下必须可用的资源
Network Only 只从网络请求资源,不使用缓存。 保证获取最新的资源 离线不可用 对实时性要求极高,且不需要离线支持的资源
Stale-While-Revalidate 先从缓存中获取资源,同时发起网络请求更新缓存。下次请求时,会使用更新后的缓存。 速度快,离线可用,下次请求时可以获取最新的资源 第一次请求时可能显示旧版本 静态资源,对实时性要求不高,但希望尽快更新的资源

第五站:消息传递:Service Worker 和网页的沟通桥梁

Service Worker 运行在独立的线程中,不能直接访问网页的 DOM。 如果需要和网页进行交互,需要使用消息传递机制。

  • 从网页发送消息给 Service Worker

    // main.js
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.ready.then(registration => {
        registration.active.postMessage({message: 'Hello from the page!'});
      });
    }

    解释一下:

    • navigator.serviceWorker.ready: 返回一个 Promise,当 Service Worker 激活后 resolve。
    • registration.active: 获取 Service Worker 的实例。
    • postMessage(...): 发送消息给 Service Worker。
  • 在 Service Worker 中接收消息

    // sw.js
    self.addEventListener('message', event => {
      console.log('Message received from the page:', event.data);
      // Do something with the message
    });

    解释一下:

    • self.addEventListener('message', ...): 监听 message 事件,当收到网页发来的消息时触发。
    • event.data: 包含消息的内容。
  • 从 Service Worker 发送消息给网页

    // sw.js
    self.addEventListener('install', event => {
      // ... 安装过程 ...
    
      self.clients.matchAll().then(clients => {
        clients.forEach(client => {
          client.postMessage({message: 'Service Worker installed and ready!'});
        });
      });
    });

    解释一下:

    • self.clients.matchAll(): 获取所有与 Service Worker 关联的客户端(网页)。
    • clients.forEach(...): 遍历所有客户端,并向每个客户端发送消息。
  • 在网页中接收 Service Worker 发来的消息

    // main.js
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.addEventListener('message', event => {
        console.log('Message received from the Service Worker:', event.data);
        // Do something with the message
      });
    }

第六站:PWA 架构:Service Worker 的终极目标

Service Worker 是构建 PWA (Progressive Web App) 的关键技术之一。 PWA 是一种可以像原生应用一样安装在设备上的 Web 应用,具有以下特点:

  • 可靠 (Reliable): 即使在网络状况不佳的情况下也能快速加载和运行。
  • 快速 (Fast): 响应速度快,用户体验流畅。
  • 吸引人 (Engaging): 可以像原生应用一样添加到主屏幕,接收推送通知。

Service Worker 在 PWA 中主要负责以下功能:

  • 离线支持: 通过缓存静态资源,即使在离线状态下也能访问应用。
  • 后台同步: 在后台同步数据,提高应用性能。
  • 推送通知: 接收推送通知,与用户进行互动。

第七站: 实战演练:一个简单的离线缓存示例

咱们来做一个简单的例子,让你的网站在离线状态下也能显示一个 "Hello World!"。

  1. 创建 index.html 文件

    <!DOCTYPE html>
    <html>
    <head>
      <title>Service Worker Example</title>
      <link rel="stylesheet" href="style.css">
      <link rel="manifest" href="manifest.json">
    </head>
    <body>
      <h1>Hello World!</h1>
      <script src="main.js"></script>
    </body>
    </html>
  2. 创建 style.css 文件

    body {
      font-family: sans-serif;
      text-align: center;
      margin-top: 50px;
    }
  3. 创建 main.js 文件

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js')
        .then(registration => {
          console.log('Service Worker registered with scope:', registration.scope);
        })
        .catch(error => {
          console.log('Service Worker registration failed:', error);
        });
    }
  4. 创建 sw.js 文件

    const CACHE_NAME = 'hello-world-cache-v1';
    const urlsToCache = [
      '/',
      '/index.html',
      '/style.css',
      '/main.js'
    ];
    
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(cache => {
            console.log('Opened cache');
            return cache.addAll(urlsToCache);
          })
      );
    });
    
    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            if (response) {
              return response;
            }
            return fetch(event.request);
          })
      );
    });
    
    self.addEventListener('activate', event => {
      const cacheWhitelist = [CACHE_NAME];
    
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (cacheWhitelist.indexOf(cacheName) === -1) {
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });
  5. 创建 manifest.json 文件 (可选,但推荐,用于 PWA 支持)

    {
      "name": "Hello World PWA",
      "short_name": "Hello World",
      "start_url": "/",
      "display": "standalone",
      "background_color": "#ffffff",
      "theme_color": "#000000",
      "icons": [
        {
          "src": "/images/icon-192x192.png",
          "sizes": "192x192",
          "type": "image/png"
        },
        {
          "src": "/images/icon-512x512.png",
          "sizes": "512x512",
          "type": "image/png"
        }
      ]
    }
    

    确保你的项目根目录下有 images 文件夹,并且包含 icon-192x192.pngicon-512x512.png 两个图标。 (即使没有这两个图标,manifest 也能工作,但是为了成为一个完整的 PWA,建议添加)

  6. 部署到服务器

    将所有文件部署到支持 HTTPS 的服务器上。 Service Worker 必须在 HTTPS 环境下才能工作(本地开发可以用 localhost)。

  7. 测试

    在浏览器中打开你的网站,然后打开开发者工具 (F12)。 在 "Application" (或 "应用程序") 选项卡中,可以看到 Service Worker 是否已经注册并激活。

    断开网络连接,刷新页面,看看是否还能正常显示 "Hello World!"。

第八站: 踩坑指南:Service Worker 常见问题及解决方案

  • Service Worker 没有生效

    • 检查 Service Worker 文件路径是否正确。
    • 检查是否在 HTTPS 环境下运行。
    • 清除浏览器缓存,重新注册 Service Worker。
    • 检查 Service Worker 文件中是否有语法错误。
  • 缓存没有更新

    • 修改 CACHE_NAME,强制浏览器更新缓存。
    • 使用 Cache-Control 头部控制缓存行为。
    • 使用 clients.claim() 方法让 Service Worker 立即控制所有客户端。
  • Service Worker 报错

    • 仔细阅读错误信息,根据错误信息进行调试。
    • 使用开发者工具的 "Application" 选项卡中的 "Service Workers" 面板进行调试。

第九站:总结与展望

Service Worker 是一个强大的工具,可以让你构建更可靠、更快速、更吸引人的 Web 应用。 虽然学习曲线可能有点陡峭,但掌握它绝对会让你成为一个更优秀的前端工程师。

希望今天的讲座能帮助大家更好地理解 Service Worker。 下次再见!

最后的温馨提示:

  • Service Worker 的调试需要耐心和细心,多看控制台输出,多查资料。
  • Service Worker 的功能远不止这些,还有很多高级用法等待你去探索。
  • 不要害怕踩坑,踩坑是成长的必经之路。

祝大家早日成为 Service Worker 大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注