1. 概述
PWA(渐进式 Web 应用)的缓存功能是实现 "离线访问" 和 "快速加载" 的核心,但不同设备(移动设备、桌面设备)因缓存空间限制 、网络环境差异 、浏览器兼容性等问题,易出现缓存不一致、更新不及时、离线功能失效等问题。
本文档将详细说明如何通过 Service Worker + Cache API 构建跨设备兼容的缓存方案,并提供 index.html
及配套文件的完整实现,确保 PWA 在 Android、iOS、桌面端等不同环境下的缓存稳定性。
2. 核心缓存问题(跨设备场景)
在不同设备上,PWA 缓存主要面临以下挑战:
问题类型 | 具体表现 |
---|---|
缓存空间限制 | 移动设备缓存空间通常<50MB(远小于桌面端),易因缓存溢出导致关键资源被清理 |
网络环境差异 | 移动设备可能使用 2G/3G 弱网,需优先缓存关键资源;桌面端多为 Wi-Fi,可适配更灵活策略 |
缓存版本冲突 | 不同设备可能残留旧版缓存,导致部分用户看到旧内容,部分用户看到新内容 |
浏览器兼容性 | iOS Safari 对 Service Worker 支持有限,部分缓存 API 行为与 Chrome 不一致 |
3. 核心解决方案
3.1 基础缓存架构:Service Worker + Cache API
通过 Service Worker
(后台脚本)拦截网络请求,结合 Cache API
管理缓存资源,核心思路:
- 版本化缓存:为缓存命名添加版本号,避免跨设备版本冲突;
- 分策略缓存:对静态资源(CSS/JS/ 图标)、API 数据采用不同缓存策略;
- 主动更新机制:检测到新版本时,提示用户刷新,确保缓存同步;
- 设备适配:针对移动设备优化缓存空间,针对弱网环境调整缓存优先级。
3.2 完整代码实现
3.2.1 1. 缓存核心脚本:sw.js
(Service Worker 文件)
javascript
typescript
// sw.js - PWA缓存核心脚本
const CACHE_VERSION = "v3.0"; // 缓存版本号:更新时递增(如v1.0→v2.0)
const CACHE_NAME = `pwa-app-cache-${CACHE_VERSION}`;
// 1. 预缓存关键资源(所有设备通用,优先保障离线可用)
const PRECACHE_RESOURCES = [
"./index.html",
"./css/style.css",
"./js/app.js",
"./icons/icon-192x192.png",
"./icons/icon-512x512.png",
"./manifest.json"
];
// 2. 安装阶段:预缓存关键资源
self.addEventListener("install", (event) => {
// 等待预缓存完成后再激活Service Worker
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(PRECACHE_RESOURCES))
.then(() => self.skipWaiting()) // 强制激活新Service Worker(无需等待旧页面关闭)
);
});
// 3. 激活阶段:清理旧版本缓存(避免跨设备残留旧缓存)
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
// 删除所有非当前版本的缓存
const deleteOldCaches = cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name));
return Promise.all(deleteOldCaches).then(() => {
self.clients.claim(); // 接管所有打开的页面(确保新缓存立即生效)
});
})
);
});
// 4. 拦截请求:分策略处理缓存
self.addEventListener("fetch", (event) => {
const request = event.request;
const isApiRequest = request.url.includes("/api/"); // 区分API请求与静态资源
// 策略1:API数据 → 网络优先(实时性数据,离线时用缓存)
if (isApiRequest) {
event.respondWith(
fetch(request)
.then((networkRes) => {
// 网络请求成功:更新缓存(确保下次离线可用)
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, networkRes.clone());
});
return networkRes;
})
.catch(() => {
// 网络失败:返回缓存数据(若缓存不存在则返回404)
return caches.match(request).then((cacheRes) => {
return cacheRes || new Response(JSON.stringify({ code: -1, msg: "离线无缓存数据" }), {
headers: { "Content-Type": "application/json" }
});
});
})
);
}
// 策略2:静态资源 → 缓存优先(非实时资源,优先加载缓存)
else {
event.respondWith(
caches.match(request).then((cacheRes) => {
// 先返回缓存(快速加载),同时后台更新缓存(确保下次是最新资源)
const fetchPromise = fetch(request)
.then((networkRes) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, networkRes.clone());
});
return networkRes;
})
.catch(() => cacheRes); // 网络失败时仍用缓存
return cacheRes || fetchPromise; // 缓存存在则用缓存,否则等网络请求
})
);
}
});
// 5. 接收页面消息:处理设备适配(如网络环境、设备类型)
self.addEventListener("message", (event) => {
const { type, payload } = event.data;
// 示例:接收页面发送的网络类型,弱网下优先缓存更多资源
if (type === "NETWORK_TYPE") {
const networkType = payload; // 如 "2g"、"3g"、"4g"、"wifi"
if (networkType === "2g" || networkType === "3g") {
console.log("弱网环境:优先保留关键缓存,不缓存非必要资源");
// 可扩展:弱网下清理大体积非关键缓存(如高清图片)
}
}
});
3.2.2 2. 入口页面:index.html
(关联 Service Worker)
index.html
是 PWA 的入口,需实现 Service Worker 注册 、缓存更新检测 、设备网络环境上报 等功能,确保缓存方案在不同设备上生效。
html
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA跨设备缓存示例</title>
<!-- PWA基础配置:关联manifest.json -->
<link rel="manifest" href="./manifest.json">
<!-- iOS兼容性配置 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="./icons/icon-192x192.png">
<!-- Android兼容性配置 -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#4285f4">
<style>
/* 基础样式:适配跨设备显示 */
body {
margin: 0;
min-height: 100vh;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.cache-status {
margin: 20px 0;
padding: 15px;
border-radius: 8px;
background: #f5f5f5;
max-width: 300px;
text-align: center;
}
.update-btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
background: #4285f4;
color: white;
font-size: 16px;
cursor: pointer;
display: none; /* 默认隐藏,有更新时显示 */
}
</style>
</head>
<body>
<h1>PWA跨设备缓存示例</h1>
<!-- 缓存状态提示 -->
<div class="cache-status" id="cacheStatus">
正在检查缓存状态...
</div>
<!-- 缓存更新按钮 -->
<button class="update-btn" id="updateBtn">
有新更新,点击刷新
</button>
<script>
// 1. 注册Service Worker(核心:启用缓存功能)
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
const registration = await navigator.serviceWorker.register("./sw.js");
console.log("Service Worker注册成功:", registration.scope);
// 2. 检测缓存更新(新Service Worker安装时触发)
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed") {
// 新缓存已安装:提示用户刷新
document.getElementById("cacheStatus").textContent = "检测到应用更新!";
document.getElementById("updateBtn").style.display = "block";
}
});
});
// 3. 上报设备网络环境(供Service Worker适配缓存策略)
if ("connection" in navigator) {
const networkType = navigator.connection.effectiveType;
// 向Service Worker发送网络类型
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: "NETWORK_TYPE",
payload: networkType
});
document.getElementById("cacheStatus").textContent = `当前网络:${networkType},缓存策略已适配`;
}
}
// 4. 检测是否从桌面启动(独立窗口模式)
if (window.matchMedia("(display-mode: standalone)").matches) {
document.getElementById("cacheStatus").textContent = "已从桌面启动,缓存正常";
}
} catch (err) {
console.error("Service Worker注册失败:", err);
document.getElementById("cacheStatus").textContent = "缓存功能未启用(浏览器不支持)";
}
});
}
// 5. 点击更新按钮:激活新缓存并刷新页面
document.getElementById("updateBtn").addEventListener("click", async () => {
const registration = await navigator.serviceWorker.ready;
// 强制激活新Service Worker
if (registration.waiting) {
registration.waiting.postMessage({ type: "SKIP_WAITING" });
// 刷新页面,应用新缓存
window.location.reload();
}
});
</script>
</body>
</html>
3.2.3 3. PWA 配置文件:manifest.json
确保缓存的资源与 PWA 配置一致,避免启动时加载未缓存的资源:
json
css
{
"name": "PWA跨设备缓存应用",
"short_name": "PWA缓存示例",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"icons": [
{
"src": "./icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
4. 分设备缓存适配策略
4.1 移动设备(Android/iOS)适配
-
缓存空间优化:
- 仅预缓存 关键资源 (如
index.html
、核心 JS/CSS、小尺寸图标),避免缓存大文件(如高清视频、大于 1MB 的图片); - 在
sw.js
中添加 "缓存大小检测",当缓存超过 30MB 时,自动删除最早未使用的非关键资源(如旧版 API 数据)。
- 仅预缓存 关键资源 (如
-
iOS Safari 特殊处理:
- iOS 对 Service Worker 的缓存有效期较短(约 7 天无访问会清理),需在
index.html
中定期触发 "缓存激活"(如每次打开页面时发送心跳请求,维持缓存); - 避免依赖
Cache API
的matchAll
方法(iOS 部分版本存在兼容性问题),优先使用match
方法精准匹配资源。
- iOS 对 Service Worker 的缓存有效期较短(约 7 天无访问会清理),需在
4.2 桌面设备适配
-
缓存策略灵活化:
- 桌面设备缓存空间充足(通常>1GB),可缓存更多非关键资源(如历史 API 数据、高清图片),提升二次加载速度;
- 在
sw.js
中通过navigator.userAgent
检测桌面浏览器,为 Chrome/Edge/Firefox 分别优化缓存策略(如 Firefox 需降低缓存更新频率)。
-
多窗口同步:
- 桌面端可能同时打开多个 PWA 窗口,需在
sw.js
的activate
事件中调用self.clients.claim()
,确保所有窗口同步使用新缓存。
- 桌面端可能同时打开多个 PWA 窗口,需在
5. 依赖文件与目录结构
确保项目文件结构如下,避免缓存资源路径错误:
plaintext
bash
pwa-cache-demo/
├─ index.html # 入口页面(注册Service Worker)
├─ sw.js # 缓存核心脚本(Service Worker)
├─ manifest.json # PWA基础配置
├─ css/
│ └─ style.css # 页面样式(预缓存资源)
├─ js/
│ └─ app.js # 业务逻辑(预缓存资源)
└─ icons/
├─ icon-192x192.png # 移动端图标(预缓存资源)
└─ icon-512x512.png # 桌面端图标(预缓存资源)
6. 部署与使用说明
6.1 部署要求
- HTTPS 环境 :PWA 的 Service Worker 仅在 HTTPS(或
localhost
本地开发环境)下生效,部署时需配置 SSL 证书; - 资源路径 :确保
sw.js
中预缓存的资源路径与实际部署路径一致(如子目录部署需调整start_url
和缓存资源路径)。
6.2 兼容性支持
平台 / 浏览器 | 支持情况 | 备注 |
---|---|---|
Android Chrome ≥57 | 完全支持(缓存策略、自动更新、离线访问) | 推荐主力平台 |
iOS Safari ≥11.3 | 部分支持(需手动开启 Service Worker) | 需定期触发缓存激活,避免自动清理 |
桌面 Chrome ≥56 | 完全支持 | 可安装为独立窗口,缓存稳定性最佳 |
桌面 Edge ≥79 | 完全支持(与 Chrome 内核一致) | 缓存策略与 Chrome 共享 |
Firefox ≥67 | 部分支持(缓存更新频率需降低) | 不支持 "强制激活新 Service Worker" |
7. 常见问题排查
问题现象 | 可能原因 | 解决方案 |
---|---|---|
缓存不更新 | 1. Service Worker 版本号未递增;2. 新资源路径未加入预缓存 | 1. 修改 sw.js 中 CACHE_VERSION ;2. 补充预缓存资源 |
iOS 离线无法访问 | 1. Service Worker 被系统清理;2. 资源路径错误 | 1. 在 index.html 中添加定期缓存心跳;2. 检查 start_url 是否正确 |
桌面端多窗口缓存不一致 | 未调用 self.clients.claim() |
在 sw.js 的 activate 事件中添加 self.clients.claim() |
缓存空间溢出 | 移动设备缓存超过限制 | 在 sw.js 中添加缓存大小检测,自动清理非关键资源 |
8. 总结
通过 "版本化缓存 + 分策略适配 + 主动更新" 的方案,可有效解决 PWA 在不同设备上的缓存问题:
- 基础层:用
Service Worker + Cache API
构建统一缓存架构; - 适配层:针对移动设备的缓存空间、iOS 的兼容性、桌面端的多窗口场景做定制化处理;
- 监控层:通过
index.html
检测缓存状态,及时提示用户更新,确保跨设备体验一致。