JS `Service Worker` `Cache Storage` `Streams API` 组合:动态内容生成与缓存

嘿,各位!今天咱们来聊点儿“高级”玩意儿,但保证不让你听得打瞌睡。我们要把Service Worker、Cache Storage和Streams API这仨家伙凑一块儿,搞点儿动态内容生成和缓存的花活儿。别怕,听起来唬人,其实挺好玩儿的。

第一部分:Service Worker,你的网页小保镖

Service Worker 是个啥?简单说,它就像你网页的小保镖,默默地在你网页和网络之间站岗放哨。它拦截你的网络请求,然后决定是放行,还是自己动手丰衣足食,从缓存里给你弄点儿东西出来。

  • 注册Service Worker:

    首先,得告诉浏览器,咱们要启用这个小保镖。在你的主JS文件里,加上这段:

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
          console.log('Service Worker 注册成功:', registration);
        })
        .catch(error => {
          console.log('Service Worker 注册失败:', error);
        });
    } else {
      console.log('您的浏览器不支持 Service Workers。');
    }

    这段代码检查浏览器是否支持Service Worker,如果支持,就注册一个名为 service-worker.js 的文件。

  • Service Worker生命周期:

    Service Worker 有一套自己的生命周期,包括 installactivate 等阶段。咱们重点关注这俩:

    • install 事件: 这是Service Worker第一次“露面”的时候。通常在这里我们会预缓存一些静态资源,比如CSS、JS、图片啥的。

      const CACHE_NAME = 'my-site-cache-v1';
      const urlsToCache = [
        '/',
        '/index.html',
        '/style.css',
        '/script.js',
        '/image.png'
      ];
      
      self.addEventListener('install', event => {
        event.waitUntil(
          caches.open(CACHE_NAME)
            .then(cache => {
              console.log('已打开缓存');
              return cache.addAll(urlsToCache);
            })
        );
      });

      这段代码创建了一个名为 my-site-cache-v1 的缓存,然后把 urlsToCache 数组里的资源一股脑儿塞进去。

    • activate 事件: 这个事件发生在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 里的缓存。

  • 拦截网络请求:fetch 事件

    重头戏来了!Service Worker 的核心功能就是拦截网络请求,并决定如何处理。

    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            // 缓存命中 - 直接返回缓存
            if (response) {
              return response;
            }
    
            // 缓存未命中 - 发起网络请求
            return fetch(event.request).then(
              function(response) {
                // 检查是否收到了有效响应
                if(!response || response.status !== 200 || response.type !== 'basic') {
                  return response;
                }
    
                // 重要:克隆 response。response 只能消费一次
                var responseToCache = response.clone();
    
                caches.open(CACHE_NAME)
                  .then(function(cache) {
                    cache.put(event.request, responseToCache);
                  });
    
                return response;
              }
            );
          })
      );
    });

    这段代码首先尝试从缓存中查找请求的资源。如果找到了,就直接返回缓存的内容。如果没找到,就发起网络请求,并将响应存入缓存,以便下次使用。注意:response 只能读取一次,所以要 clone() 一份用于缓存。

第二部分:Cache Storage,你的网页小仓库

Cache Storage 是浏览器提供的一个用于存储网络请求响应的仓库。它可以存储各种类型的资源,比如 HTML、CSS、JS、图片等等。

  • 打开缓存:

    caches.open('my-cache-name')
      .then(cache => {
        console.log('已打开缓存');
      });
  • 存储资源:

    cache.put('/my-resource.html', new Response('<h1>Hello, Cache!</h1>'));
  • 读取资源:

    cache.match('/my-resource.html')
      .then(response => {
        if (response) {
          return response.text();
        } else {
          return '资源未找到';
        }
      })
      .then(data => {
        console.log(data); // 输出 "<h1>Hello, Cache!</h1>"
      });
  • 删除缓存:

    caches.delete('my-cache-name')
      .then(success => {
        console.log('缓存删除成功:', success);
      });

第三部分:Streams API,你的数据传送带

Streams API 允许你以流的方式处理数据,而不是一次性加载整个文件。这对于处理大型文件或者需要实时生成的内容非常有用。

  • 创建ReadableStream:

    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue('<h1>Part 1</h1>');
        controller.enqueue('<h2>Part 2</h2>');
        controller.close();
      }
    });

    这段代码创建了一个 ReadableStream,它会依次输出 "

    Part 1

    " 和 "

    Part 2

    "。

  • 使用ReadableStream:

    fetch('/my-stream-endpoint')
      .then(response => {
        const reader = response.body.getReader();
        return new ReadableStream({
          start(controller) {
            function push() {
              reader.read().then(({ done, value }) => {
                if (done) {
                  controller.close();
                  return;
                }
                controller.enqueue(new TextDecoder().decode(value));
                push();
              });
            }
            push();
          }
        });
      })
      .then(stream => {
        // 处理 stream
      });

    这段代码从 /my-stream-endpoint 获取一个响应,然后将响应的 body 转换成 ReadableStream

第四部分:三剑客合璧:动态内容生成与缓存

现在,让我们把这三位大佬凑一块儿,看看能搞出什么名堂。假设我们需要动态生成一些HTML内容,并且缓存起来,以便下次快速加载。

  • 服务器端代码 (Node.js为例):

    const http = require('http');
    
    const server = http.createServer((req, res) => {
      if (req.url === '/dynamic-content') {
        res.setHeader('Content-Type', 'text/html; charset=utf-8');
        res.setHeader('Cache-Control', 'no-cache'); // 重要:禁止服务器端缓存
    
        const stream = new ReadableStream({
          start(controller) {
            controller.enqueue('<!DOCTYPE html>n');
            controller.enqueue('<html>n');
            controller.enqueue('<head>n');
            controller.enqueue('<title>Dynamic Content</title>n');
            controller.enqueue('</head>n');
            controller.enqueue('<body>n');
            controller.enqueue('<h1>动态生成的内容</h1>n');
            controller.enqueue('<p>当前时间:' + new Date().toLocaleTimeString() + '</p>n');
            controller.enqueue('</body>n');
            controller.enqueue('</html>n');
            controller.close();
          }
        });
    
        const encoder = new TextEncoder();
        const transformStream = new TransformStream({
          transform(chunk, controller) {
            controller.enqueue(encoder.encode(chunk));
          }
        });
    
        stream
          .pipeThrough(transformStream)
          .pipeTo(new WritableStream({
            write(chunk) {
              res.write(Buffer.from(chunk));
            },
            close() {
              res.end();
            },
            abort(err) {
              console.error("管道出错:", err);
              res.statusCode = 500;
              res.end('服务器错误');
            }
          }));
    
      } else {
        res.statusCode = 404;
        res.end('Not Found');
      }
    });
    
    server.listen(3000, () => {
      console.log('服务器运行在 http://localhost:3000/');
    });

    这段代码创建一个简单的HTTP服务器,当请求 /dynamic-content 时,它会动态生成HTML内容,并通过 ReadableStream 发送给客户端。 Cache-Control: no-cache 是关键,防止浏览器直接缓存服务器返回的内容,让Service Worker接管缓存。 同时使用了 TransformStream 将字符串块转换为 Uint8Array,因为 res.write 期望的是 Buffer 或 Uint8Array。

  • Service Worker 代码:

    const CACHE_NAME = 'dynamic-content-cache-v1';
    
    self.addEventListener('fetch', event => {
      if (event.request.url.includes('/dynamic-content')) {
        event.respondWith(
          caches.match(event.request)
            .then(response => {
              if (response) {
                console.log('从缓存中返回动态内容');
                return response;
              }
    
              console.log('从网络获取动态内容');
              return fetch(event.request)
                .then(networkResponse => {
                  // 确保响应成功
                  if (!networkResponse || networkResponse.status !== 200) {
                    return networkResponse;
                  }
    
                  // 克隆响应,因为我们要读取它两次:一次用于返回,一次用于缓存
                  const responseToCache = networkResponse.clone();
    
                  caches.open(CACHE_NAME)
                    .then(cache => {
                      cache.put(event.request, responseToCache);
                      console.log('动态内容已缓存');
                    });
    
                  return networkResponse;
                });
            })
        );
      }
    });

    这段代码拦截所有包含 /dynamic-content 的请求。如果缓存中存在,就直接返回缓存的内容。如果缓存中不存在,就发起网络请求,并将响应存入缓存。注意,我们需要克隆响应,因为 response 只能读取一次。

  • 客户端代码:

    <!DOCTYPE html>
    <html>
    <head>
      <title>Dynamic Content Example</title>
    </head>
    <body>
      <div id="content">Loading...</div>
    
      <script>
        fetch('/dynamic-content')
          .then(response => response.text())
          .then(data => {
            document.getElementById('content').innerHTML = data;
          });
    
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {
              console.log('Service Worker registered:', registration);
            })
            .catch(error => {
              console.error('Service Worker registration failed:', error);
            });
        }
      </script>
    </body>
    </html>

    这段代码发起一个对 /dynamic-content 的请求,并将返回的HTML内容显示在页面上。 同时注册了Service Worker。

运行步骤:

  1. 启动服务器: 运行你的Node.js服务器。
  2. 部署文件:index.htmlservice-worker.js 文件放在服务器可以访问的目录下。
  3. 访问页面: 在浏览器中打开 index.html

结果:

第一次加载时,Service Worker会从网络获取动态内容,并将其缓存起来。后续加载时,Service Worker会直接从缓存中返回动态内容,从而加快加载速度。 你会注意到,即使服务器端的时间一直在更新,客户端显示的时间也会是第一次加载时的时间,因为是从缓存读取的。

总结:

技术 作用 代码示例
Service Worker 拦截网络请求,管理缓存 self.addEventListener('fetch', event => { ... })
Cache Storage 存储网络请求的响应 caches.open('my-cache-name').then(cache => { ... })
Streams API 以流的方式处理数据,用于动态生成内容 new ReadableStream({ start(controller) { ... } })
服务器端 (Node.js) 生成动态内容并发送给客户端,设置 Cache-Control: no-cache 防止服务器缓存 javascript res.setHeader('Cache-Control', 'no-cache'); const stream = new ReadableStream({ ... }); stream.pipeThrough(...).pipeTo(...)

注意事项:

  • 缓存策略: 根据你的需求选择合适的缓存策略。例如,你可以使用 "Cache-First" 策略(优先使用缓存,如果缓存不存在则从网络获取),或者 "Network-First" 策略(优先从网络获取,如果网络不可用则使用缓存)。
  • 缓存更新: 定期更新缓存,以确保用户始终获得最新的内容。可以通过版本号管理缓存,当版本号更新时,删除旧缓存并创建新缓存。
  • 错误处理: 在Service Worker中添加错误处理逻辑,以应对网络错误或其他异常情况。
  • 调试: 使用浏览器的开发者工具调试Service Worker。Chrome的 "Application" 面板提供了Service Worker和Cache Storage的相关信息。
  • CORS: 如果你的动态内容是从不同的域名获取的,请确保服务器端正确配置了 CORS。

进阶玩法:

  • 使用Streams API生成更复杂的内容: 例如,你可以从多个数据源获取数据,并将它们组合成一个流。
  • 实现离线访问: 利用Service Worker和Cache Storage,让你的网站在离线状态下也能访问。
  • 推送通知: 使用Service Worker接收服务器推送的通知,并显示在用户的设备上。

好了,今天的讲座就到这里。希望大家有所收获,也希望大家能把这些技术应用到实际项目中,创造出更棒的Web应用!记住,编程就像玩乐高,把不同的模块拼在一起,就能创造出无限可能!

发表回复

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