背景
在2024年国庆前后,大A行情出现了一波高潮,国庆期间利好消息影响韭菜情绪造成拥堵开户的场景。对网络和后端造成巨大的压力,同时也影响用户体验,这种突发情况一般是网络组会采用扩容的方式进行应对,但由于内部硬件资源不充裕、资源调配不均匀,所以需要前端同事一起进行优化。
我这边最终组织团队决定使用Service Worker对产品部分高频调用的接口(如行情、指标、基础信息等)进行缓存。理由是此种方式对于产品的源码改动最小化,通过前端缓存策略,可以有效减少重复请求、提升响应速度。
本文结合实际 Service Worker 源码,介绍如何对高频接口进行缓存,并兼容 GET/POST 场景。
方案概述
- GET 请求:采用 Cache Storage,结合缓存有效期与最大缓存条数控制。
- POST 请求:采用 IndexedDB,解决 Cache Storage 仅支持 GET 的限制,支持请求体作为缓存 key。
关键实现
1. Service Worker 拦截与分流
self.addEventListener('fetch', function (event) {
const resUrl = event.request.url;
// 高频 GET 接口缓存
if (resUrl.indexOf('finance/stock/maintable/index_merge') > -1) {
const expires = 1 * 60 * 1000;
handleServerCahce(event, 'INDXEX_MERGE', expires);
}
// ... 其他 GET 接口 ...
// 高频 POST 接口缓存
if (event.request.method === 'POST' && resUrl.indexOf('statistic/component/ptext') > -1) {
handlePostWithIDBCache(event, 'P_TEXT_POST');
}
});
- 通过 URL 关键字精准匹配高频接口。
- GET/POST 分别调用不同缓存处理函数。
2. GET 接口缓存(Cache Storage)
function handleServerCahce(event, cache_name, expires, cache_length = 10) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
const responseDate = response.headers.get('Date');
const FiveMinutesLater = new Date(responseDate).getTime() + expires;
if (Date.now() <= FiveMinutesLater) {
return response;
}
}
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(function (response) {
if (!response || response.status !== 200) return response;
var responseToCache = response.clone();
caches.open(cache_name).then(function (cache) {
checkCacheSize(cache, cache_length);
cache.put(event.request, responseToCache);
});
return response;
});
})
);
}
- 命中缓存且未过期直接返回,未命中或过期则请求网络并更新缓存。
- 支持自定义缓存有效期和最大缓存条数。
3. POST 接口缓存(IndexedDB)
function handlePostWithIDBCache(event, storeName) {
event.respondWith((async () => {
const cacheKey = await event.request.clone().text(); // 用请求体作为key
const cached = await idbGet(storeName, cacheKey);
if (cached) {
return new Response(cached.body, cached.options);
}
const response = await fetch(event.request.clone());
if (response && response.status === 200) {
const body = await response.clone().text();
const options = {
status: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries())
};
await idbSet(storeName, cacheKey, { body, options });
}
return response;
})());
}
- 以请求体为 key,解决 POST 请求幂等性和参数多样性问题。
- 通过 IndexedDB 存储和读取缓存,突破 Cache Storage 限制。
4. IndexedDB 简单封装
function idbGet(store, key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sw-db', 1);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains(store)) {
db.createObjectStore(store);
}
};
request.onsuccess = function (event) {
const db = event.target.result;
const tx = db.transaction(store, 'readonly');
const storeObj = tx.objectStore(store);
const getReq = storeObj.get(key);
getReq.onsuccess = function () {
resolve(getReq.result);
};
getReq.onerror = function () {
resolve(undefined);
};
};
request.onerror = function () {
resolve(undefined);
};
});
}
function idbSet(store, key, value) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sw-db', 1);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains(store)) {
db.createObjectStore(store);
}
};
request.onsuccess = function (event) {
const db = event.target.result;
const tx = db.transaction(store, 'readwrite');
const storeObj = tx.objectStore(store);
storeObj.put(value, key);
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
resolve();
};
};
request.onerror = function () {
resolve();
};
});
}
- 支持动态建表,异步读写,便于扩展和维护。
前端页面注册 Service Worker
在你的 HTML 文件中添加如下脚本,确保 Service Worker 能被正确注册和激活:
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("./sw.js").then(
function (registration) {
console.log("ServiceWorker registration successful");
},
function (err) {
console.error("ServiceWorker registration failed: ", err);
}
);
});
}
</script>
实践建议
- 缓存粒度:仅对高频、数据量适中的接口做缓存,避免缓存过大或数据一致性风险。
- 缓存上限:根据应用内存具体情况(浏览器、客户端webview)设置数据大小上限,防止无限膨胀。
- 缓存失效:合理设置 expires,防止数据陈旧。
- 调试与监控:可通过浏览器 DevTools Application 面板监控缓存命中与存储情况。
本方案通过 Service Worker 拦截高频接口请求,结合 Cache Storage 和 IndexedDB,实现了 GET/POST 场景下的高效缓存。既提升了用户体验,也减轻了后端压力,适用于大部分前端高频数据场景,实际项目可根据接口特性灵活调整缓存策略。