2026年,PWA已不是“未来趋势”,而是用户留存的生死线。深入整合IndexedDB与Service Worker,手把手教你实现数据持久化、请求队列、后台同步,让Web应用在地铁隧道、电梯间、飞机上依然流畅运行——这才是真正的“全站离线”。
引言:你的Web应用真的“离线可用”吗?
你有没有遇到过这样的场景?用户在填写一份2000字的反馈表单,突然进入电梯,网络中断,页面刷新后——所有内容清零。用户怒砸手机,卸载应用。
或者,你的电商网站商品页加载了3秒才出内容,用户直接关闭。你丢掉的不是一次访问,是一次转化、一个客户、一份信任。
你可能已经用了 localStorage 缓存数据,也上了HTTPS,甚至注册了Service Worker,但为什么还是“伪离线”?
因为你只缓存了静态资源,没管动态数据。
因为你没处理网络中断时的写请求。
因为你没在后台悄悄同步用户操作。
这不是PWA,这是“PWA演示页”。
真正的PWA,不是“能添加到主屏”就行,而是:
✅ 随时可用(即使无网络)
✅ 数据不丢(断网也能写)
✅ 体验流畅(秒开、动画顺滑)
✅ 后台同步(恢复网络后自动上传)
今天,我们就用最硬核的实战方式,带你用IndexedDB + Service Worker,从零构建一个真正的全站离线Web应用。
这不是“能用就行”,这是让用户忘记自己在用浏览器。
🧩 第一阶段:PWA离线架构全景图
✅ 1. 离线能力的三大层级
| 层级 |
能力 |
技术方案 |
| L1:静态资源缓存 |
HTML/CSS/JS/图片离线加载 |
Service Worker + Cache API |
| L2:动态数据持久化 |
用户数据、表单、状态本地存储 |
IndexedDB |
| L3:离线写入 & 后台同步 |
断网时操作,恢复后自动提交 |
IndexedDB + Service Worker + Sync Manager |
💡 关键认知:
L1是“能打开”,L2是“能用”,L3是“能信任”。
用户只关心:我操作了,它就得记住。
✅ 2. 核心技术栈
- Service Worker:网络代理,拦截请求,控制缓存
- IndexedDB:结构化存储,存用户数据、待同步队列
- Cache API:静态资源缓存
- Background Sync / Periodic Sync:网络恢复后自动同步
- Web App Manifest:添加到主屏,全屏体验
🛠 第二阶段:Service Worker —— 网络的“中间人”
✅ 1. 基础注册与安装
// main.js
if (‘serviceWorker‘ in navigator) {
navigator.serviceWorker.register(’/sw.js’)
.then(reg => console.log(‘SW registered:’, reg.scope))
.catch(err => console.error(‘SW registration failed:’, err));
}
✅ 2. 缓存静态资源(L1)
// sw.js
const CACHE_NAME = ‘static-v3’;
const STATIC_FILES = [
’/’,
’/index.html’,
’/app.js’,
’/style.css’,
’/images/logo.png’
];
self.addEventListener(‘install’, event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_FILES))
);
});
self.addEventListener(‘fetch’, event => {
// 只缓存同源 GET 请求
if (event.request.method === ‘GET’ && event.request.url.includes(self.location.origin)) {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
}
});
✅ 优化:使用 stale-while-revalidate 策略,优先返回缓存,后台更新。
💾 第三阶段:IndexedDB —— 数据的“保险箱”
✅ 1. 设计离线数据模型
// 存储用户操作队列
const DB_NAME = ‘PwaOfflineDB’;
const DB_VERSION = 2;
const STORES = {
data: ‘data’, // 正常数据(如笔记、订单)
queue: ‘sync_queue’ // 待同步的操作队列
};
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
req.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(‘data’)) {
db.createObjectStore(‘data’, { keyPath: ‘id’ });
}
if (!db.objectStoreNames.contains(‘sync_queue’)) {
const queueStore = db.createObjectStore(‘sync_queue’, { autoIncrement: true });
queueStore.createIndex(‘timestamp’, ‘timestamp’, { unique: false });
}
};
});
}
✅ 2. 离线写入:先存本地,再入队
// app.js
async function saveDataLocally(data) {
const db = await openDB();
const tx = db.transaction(‘data’, ‘readwrite’);
const store = tx.objectStore(‘data’);
store.put({ …data, id: Date.now() });
await tx.done;
// 加入同步队列
await addToSyncQueue(‘POST’, ‘/api/data’, data);
console.log(‘数据已离线保存,等待同步’);
}
function addToSyncQueue(method, url, payload) {
const db = await openDB();
const tx = db.transaction(‘sync_queue’, ‘readwrite’);
const store = tx.objectStore(‘sync_queue’);
store.add({ method, url, payload, timestamp: Date.now(), status: ‘pending’ });
return tx.done;
}
🔄 第四阶段:Service Worker + IndexedDB 联动 —— 后台同步
✅ 1. 使用 Background Sync API(需 HTTPS)
// app.js
async function syncLater() {
if (‘sync’ in navigator) {
try {
await navigator.serviceWorker.ready;
await navigator.serviceWorker.controller.postMessage({ type: ‘sync’ });
// 或使用 Sync Manager
await navigator.serviceWorker.ready.then(reg => {
reg.sync.register(‘sync-data’);
});
} catch (err) {
console.warn(‘Sync not supported or failed:’, err);
}
}
}
// sw.js
self.addEventListener(‘sync’, event => {
if (event.tag === ‘sync-data’) {
event.waitUntil(syncDataFromQueue());
}
});
async function syncDataFromQueue() {
const db = await openDB();
const tx = db.transaction(‘sync_queue’, ‘readwrite’);
const store = tx.objectStore(‘sync_queue’);
const requests = await store.index(‘timestamp’).getAll();
for (const req of requests) {
try {
const res = await fetch(req.url, {
method: req.method,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(req.payload)
});
if (res.ok) {
// 从队列删除
await store.delete(req.id);
}
} catch (err) {
console.log(‘同步失败,保留队列:’, req);
// 可设置重试次数,避免无限重试
}
}
await tx.done;
}
✅ 2. 网络恢复时自动触发同步
// app.js
window.addEventListener(‘online’, () => {
console.log(‘网络恢复,触发同步’);
syncLater();
});
🚀 第五阶段:性能与体验优化
✅ 1. 优先使用本地数据(Stale-While-Revalidate)
async function getData(url) {
// 1. 先从 IndexedDB 读
const localData = await getDataFromDB(url);
if (localData) render(localData);
// 2. 后台拉取最新数据
try {
const res = await fetch(url);
const remoteData = await res.json();
await saveDataToDB(remoteData);
render(remoteData); // 更新 UI
} catch (err) {
// 用本地数据兜底
console.log(‘使用离线数据’, err);
}
}
✅ 2. 提供离线UI反馈
- 网络中断时显示“您当前处于离线状态”
- 同步成功后弹出“数据已同步”
- 表单提交后显示“已保存,等待上传”
✅ 3. 存储配额管理
- 使用
navigator.storage.estimate() 监控使用量
- 大数据分片存储或压缩
- 定期清理过期缓存
🛡 第六阶段:安全与兼容性边界
✅ 安全事项
- 必须HTTPS:Service Worker和Background Sync仅在安全上下文可用
- XSS防护:IndexedDB可被同源脚本读取,敏感数据需加密
- CSRF防护:同步请求需携带token,防止重放攻击
✅ 兼容性处理
- Safari不支持Background Sync:降级为
online 事件触发
- 旧浏览器不支持IndexedDB:降级为localStorage + 简单队列
- 使用App Shell架构:确保核心UI能快速加载
🎯 结语:PWA的终极目标,是让用户忘记“网络”的存在
在2026年,“离线”不是异常,而是常态。
用户在地铁、电梯、山区、飞机上使用你的应用,
你不该要求他们“等有网再试”,
而该说:“没关系,我们帮你存着,网一通就发出去。”
这才是PWA的灵魂:以用户为中心,不以网络为边界。
而实现它的技术,就藏在你每天写的 fetch 和 indexedDB.open 之间。
记住:
别再做“有网才可用”的Web应用了。
从今天起,做真正的“永远在线”的Web体验。
希望这篇详尽的实战指南能为你构建可靠的应用体验提供清晰的路径。如果你在实践前端工程化或其他技术领域遇到挑战,欢迎在云栈社区交流探讨,我们共同进步。