嘿,各位!今天咱们来聊点儿“高级”玩意儿,但保证不让你听得打瞌睡。我们要把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 有一套自己的生命周期,包括
install
、activate
等阶段。咱们重点关注这俩:-
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。
运行步骤:
- 启动服务器: 运行你的Node.js服务器。
- 部署文件: 将
index.html
和service-worker.js
文件放在服务器可以访问的目录下。 - 访问页面: 在浏览器中打开
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应用!记住,编程就像玩乐高,把不同的模块拼在一起,就能创造出无限可能!