构建支持PWA的离线优先应用策略,核心是通过Service Worker拦截网络请求实现资源缓存,结合本地存储方案保存业务数据,让应用在无网络或弱网环境下依然可以正常运行,同时保证网络恢复后数据能同步到服务端。

离线优先策略的核心组成
离线优先策略主要包含三个部分,分别是资源缓存管理、本地数据存储、数据同步机制,三者配合才能实现完整的离线使用体验。
1. Service Worker与资源缓存
Service Worker是运行在浏览器后台的脚本,独立于主线程,能够拦截页面的网络请求,是实现离线优先的基础。首先需要完成Service Worker的注册,代码如下:
// 注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
// 注册sw.js文件,作用域为当前页面所在目录
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker注册成功,作用域:', registration.scope);
})
.catch((error) => {
console.log('Service Worker注册失败:', error);
});
});
}
Service Worker的生命周期分为安装、激活、拦截请求三个阶段,我们需要在安装阶段缓存核心静态资源,在激活阶段清理旧版本缓存,在fetch事件中根据请求类型选择缓存优先或网络优先的策略。
以下是sw.js中的核心逻辑示例:
// 缓存名称,添加版本号方便后续更新
const CACHE_NAME = 'offline-pwa-v1';
// 需要预缓存的核心静态资源
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/css/style.css',
'/js/main.js',
'/images/logo.png'
];
// 安装阶段:预缓存核心资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('预缓存核心资源');
return cache.addAll(PRECACHE_ASSETS);
})
.then(() => {
// 跳过等待,直接进入激活阶段
return self.skipWaiting();
})
);
});
// 激活阶段:清理旧版本缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 删除不是当前版本的缓存
if (cacheName !== CACHE_NAME) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
// 立即接管所有页面
return self.clients.claim();
})
);
});
// 拦截请求阶段:实现缓存优先策略
self.addEventListener('fetch', (event) => {
// 只处理GET请求
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
// 如果缓存中有对应资源,直接返回缓存
if (cachedResponse) {
return cachedResponse;
}
// 缓存中没有则发起网络请求
return fetch(event.request)
.then((networkResponse) => {
// 网络请求成功,将资源存入缓存
if (networkResponse && networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseClone);
});
}
return networkResponse;
})
.catch(() => {
// 网络请求失败,返回离线兜底页面
return caches.match('/offline.html');
});
})
);
});
2. 本地数据存储方案
除了静态资源缓存,业务数据的离线存储需要使用IndexedDB,它支持存储大量结构化数据,比localStorage更适合离线场景。我们可以封装一个简单的IndexedDB操作工具:
// IndexedDB操作工具类
class DBHelper {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
// 打开数据库
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
// 数据库版本升级时创建对象存储空间
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
// 创建存储空间,使用id作为主键
db.createObjectStore(this.storeName, { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
reject('打开数据库失败:' + event.target.error);
};
});
}
// 存储数据
saveData(data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(data);
request.onsuccess = () => resolve('数据存储成功');
request.onerror = (event) => reject('存储失败:' + event.target.error);
});
}
// 获取数据
getData(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject('获取数据失败:' + event.target.error);
});
}
// 获取所有数据
getAllData() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject('获取所有数据失败:' + event.target.error);
});
}
}
// 使用示例:初始化数据库
const dbHelper = new DBHelper('offline-pwa-db', 'user-data');
dbHelper.openDB().then(() => {
console.log('数据库初始化完成');
});
3. 数据同步机制
离线状态下用户产生的数据需要暂存在本地,当网络恢复后自动同步到服务端。我们可以通过监听网络状态变化,结合Service Worker的消息机制实现同步:
// 监听网络状态变化
window.addEventListener('online', () => {
console.log('网络已恢复,开始同步数据');
syncLocalData();
});
// 同步本地数据到服务端
async function syncLocalData() {
try {
// 从IndexedDB获取所有待同步的数据
const localDataList = await dbHelper.getAllData();
for (const item of localDataList) {
if (item.needSync) {
// 发送数据到服务端
const response = await fetch('/api/save-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
});
if (response.ok) {
// 同步成功,更新本地数据状态
item.needSync = false;
await dbHelper.saveData(item);
console.log('数据同步成功,id:', item.id);
}
}
}
} catch (error) {
console.log('数据同步失败:', error);
}
}
// 离线时保存数据到本地,标记为需要同步
async function saveDataOffline(data) {
const dataWithSyncFlag = {
...data,
id: Date.now().toString(),
needSync: true,
createTime: new Date().toISOString()
};
await dbHelper.saveData(dataWithSyncFlag);
console.log('数据已保存到本地,等待网络恢复后同步');
}
缓存策略的选择建议
不同的资源类型适合不同的缓存策略,我们可以根据实际场景调整,以下是常见策略的对比:
| 策略类型 | 适用场景 | 实现逻辑 |
|---|---|---|
| 缓存优先 | 静态资源、不常变化的接口数据 | 先查缓存,缓存存在直接返回,不存在再请求网络并缓存 |
| 网络优先 | 实时性要求高的接口数据 | 先请求网络,成功则返回并缓存,失败则返回缓存数据 |
| 仅缓存 | 预缓存的核心离线资源 | 只从缓存获取,不发起网络请求 |
| 缓存并更新 | 需要离线使用但也要保持最新的资源 | 先返回缓存,同时发起网络请求更新缓存 |
注意事项
- Service Worker的注册作用域需要注意,默认是sw.js所在目录,避免缓存范围过大或过小
- 缓存版本更新时需要及时清理旧缓存,避免用户一直使用旧版本资源
- IndexedDB的操作是异步的,需要注意处理异常情况,避免数据丢失
- 敏感数据不建议存储在本地,离线存储的数据需要做必要的加密处理
- 测试时需要清除浏览器缓存和Service Worker,避免旧逻辑影响测试结果
PWA离线优先Service_Worker缓存策略IndexedDB修改时间:2026-07-09 13:03:40