demo
下面给你一个完整可运行的 Service Worker 缓存 Demo,包含 HTML 页面和 Service Worker 脚本。我会用最简单的代码说明“具体缓存什么”以及“怎么搞的”。
1. 项目结构(两个文件就够了)
demo/
index.html # 主页面
sw.js # Service Worker 脚本
你可以把这两个文件放到同一个目录下,然后用一个本地 Web 服务器(比如 npx http-server、live server)打开 index.html,因为 Service Worker 需要 HTTPS 或 localhost 环境。
2. 代码实现
2.1 主页面 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker 缓存 Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
button { padding: 8px 16px; margin: 4px; cursor: pointer; }
.status { background: #f0f0f0; padding: 12px; border-radius: 8px; margin: 20px 0; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h1>🚀 Service Worker 缓存示例</h1>
<div class="status" id="status">检测 Service Worker 状态...</div>
<h2>测试图片(会被缓存)</h2>
<img src="https://picsum.photos/200/150?random=1" alt="随机图片1" width="200">
<img src="https://picsum.photos/200/150?random=2" alt="随机图片2" width="200">
<h2>测试 API(不会被缓存,每次都从网络请求)</h2>
<button id="fetchApiBtn">请求 /api/time 获取服务器时间</button>
<pre id="apiResult">点击按钮查看结果</pre>
<h2>离线测试说明</h2>
<ul>
<li>✅ 打开页面后,Service Worker 会缓存页面本身、CSS、JS 以及两张图片。</li>
<li>✅ 然后断网(在 DevTools → Network 勾选 Offline),刷新页面 — 图片依然能显示!</li>
<li>❌ 点击“请求 API”按钮会失败(因为 API 没被缓存,且网络离线)。</li>
</ul>
<script>
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
document.getElementById('status').innerHTML =
'✅ Service Worker 注册成功,作用域:' + reg.scope;
})
.catch(err => {
document.getElementById('status').innerHTML =
'❌ 注册失败:' + err.message;
});
} else {
document.getElementById('status').innerHTML = '❌ 浏览器不支持 Service Worker';
}
// 测试 API 按钮(普通 fetch,不经过特殊缓存逻辑)
document.getElementById('fetchApiBtn').addEventListener('click', async () => {
try {
const res = await fetch('/api/time');
const data = await res.json();
document.getElementById('apiResult').textContent =
`服务器时间:${data.time}\n响应状态:${res.status}`;
} catch (err) {
document.getElementById('apiResult').textContent =
`请求失败:${err.message} (可能离线了)`;
}
});
</script>
</body>
</html>
2.2 Service Worker 脚本 sw.js
// 缓存版本号(更新 SW 时改这个数字即可)
const CACHE_VERSION = 'v1.0';
// 需要预缓存的资源清单(核心静态资源)
const PRECACHE_URLS = [
'/', // 相当于 index.html 的路径
'/index.html',
// 如果要缓存 CSS/JS 等,可以加进来,这个 demo 没有额外文件
// 注意:图片是外部链接,不能通过 install 预缓存(跨域限制?实际可以,但需要处理,我们用 fetch 时缓存)
];
// 安装事件:预缓存关键资源
self.addEventListener('install', event => {
console.log('[SW] Install 事件', CACHE_VERSION);
event.waitUntil(
caches.open(CACHE_VERSION)
.then(cache => {
console.log('[SW] 预缓存资源');
return cache.addAll(PRECACHE_URLS);
})
.then(() => {
// 强制跳过等待,立即激活
return self.skipWaiting();
})
);
});
// 激活事件:删除旧版本缓存
self.addEventListener('activate', event => {
console.log('[SW] Activate 事件', CACHE_VERSION);
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.map(key => {
if (key !== CACHE_VERSION) {
console.log('[SW] 删除旧缓存', key);
return caches.delete(key);
}
})
);
}).then(() => {
// 立即控制所有客户端(页面)
return self.clients.claim();
})
);
});
// 请求拦截:缓存策略(Cache First,网络回退)
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
// 策略1:对于我们的 API 请求(例如 /api/time),永远从网络获取,不缓存
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).catch(err => {
// 离线时返回友好的提示
return new Response(
JSON.stringify({ error: '离线状态,无法获取 API 数据' }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
})
);
return;
}
// 策略2:对于图片等资源,使用 Cache First,然后网络回退
// 并且把网络获取的新资源加入缓存(动态缓存)
event.respondWith(
caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
console.log('[SW] 命中缓存', request.url);
return cachedResponse;
}
console.log('[SW] 未命中缓存,请求网络', request.url);
return fetch(request).then(networkResponse => {
// 只缓存 GET 请求且状态正常的响应,并且只缓存图片、CSS、JS 等(可选)
// 这里简单示例:只要不是 API 且不是其他特殊请求,都缓存
if (request.method === 'GET' && networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(CACHE_VERSION).then(cache => {
cache.put(request, responseClone);
});
}
return networkResponse;
}).catch(err => {
// 完全离线且未缓存时,可以返回一个占位图
if (request.url.match(/\.(jpg|png|gif|svg|webp)/i)) {
return new Response('图片加载失败(离线无缓存)', { status: 404 });
}
return new Response('离线状态,无法访问该资源', { status: 503 });
});
})
);
});
3. 为了让 API 请求 /api/time 能工作
上面的例子中,/api/time 是一个虚构接口。你可以用 Express、Node 或简单的 Mock。如果你想看到完整效果,可以加一个简单的静态 JSON 响应方法:
在项目根目录放一个 api 文件夹并创建 time.json?不,那样不符合 fetch 预期。简单起见:
你可以把 index.html 里的 API 调用改成访问一个真实的测试接口,比如 https://httpbin.org/json,但注意跨域问题(接口支持 CORS 就行)。
或者你只想测试缓存效果,直接注释掉 API 按钮部分,只看图片缓存。图片使用的是 picsum.photos,支持 HTTPS 和跨域,完全可用。
4. 运行测试步骤
-
启动一个本地 HTTP 服务器(因为 Service Worker 必须在 http://localhost 或 HTTPS 下):
npx http-server -p 8080或者用 VS Code 的 Live Server 插件。
-
访问
http://localhost:8080 -
打开 DevTools(F12):
- 进入 Application → Service Workers,可以看到
sw.js已注册并激活。 - 进入 Network 标签,观察请求:第一次加载时图片来自网络,第二次刷新时图片从 Service Worker 缓存返回(会有
(from ServiceWorker)标记)。
- 进入 Application → Service Workers,可以看到
-
测试离线:
- 在 DevTools 的 Network 标签勾选 Offline。
- 刷新页面 → 页面本身(HTML)和两张图片依然正常显示!因为被缓存了。
- 点击“请求 API”按钮 → 会失败(返回 503 离线消息),这正是预期行为。
5. 代码解释:到底缓存了什么?怎么搞的?
| 步骤 | 做什么 | 代码位置 |
|---|---|---|
| 1. 注册 | 页面调用 navigator.serviceWorker.register('/sw.js') | index.html 的 <script> |
| 2. Install | SW 的 install 事件里 cache.addAll(PRECACHE_URLS) 预缓存了 / 和 /index.html | sw.js 的 install |
| 3. Activate | 删除旧版本缓存,避免无效数据堆积 | sw.js 的 activate |
| 4. Fetch | 拦截所有请求,如果是 API 路径 → 网络优先,绝不缓存;其他 GET 请求 → 先查缓存,没有再取网络,并动态添加到缓存 | sw.js 的 fetch |
核心缓存策略:
- 预缓存:在 SW 安装时就把页面骨架存起来(确保离线时能看到页面)。
- 动态缓存:访问过的图片、CSS 等资源在第一次网络请求后自动存入缓存,下次离线可用。
- 不缓存写操作:API 请求故意不走缓存,避免脏数据。
6. 常见问题
-
为什么我的图片还是从网络加载?
检查 Console 是否有 SW 注册失败的错误(路径不对、不支持 localhost?)。 -
如何更新缓存?
修改CACHE_VERSION变量(比如v2.0),重新刷新页面,SW 会安装新版本,激活时删除旧缓存。 -
跨域资源可以缓存吗?
可以,但要注意fetch时如果资源不支持 CORS,会得到opaque响应,缓存它没问题,只是无法查看内容大小/状态。
这个 demo 涵盖了最常用、最安全的缓存模式,你可以直接复制运行测试。如果需要更高级的 “Network First” 或 “Stale While Revalidate” 策略,我可以再帮你扩展。