工作中开发的网站,请求一次要很长时间,对于查询条件相同的请求,期望使用上一次的响应结果,不再重发请求。
之前的做法是每个接口单独处理,将 api 查询参数和响应结果存在变量里,每次请求时手动判断查询参数是否变化,如果没有变化,使用存在变量里的缓存数据,否则发送请求。
经过调研,发现浏览器有两个 API 可以更优雅地实现 api 数据缓存: Service worker + Cache。这个缓存是存在硬盘里的,不会导致网页内存暴增,刷新页面也不会丢失缓存。(之前存在变量里,刷新页面缓存会丢失)
创建 sw.js,复制如下脚本内容,在入口文件中引入 sw.js 即可。
大概介绍下脚本:
默认情况下,第二天会删除前一天的缓存,保证浏览器缓存数据不会一直增长,消耗硬盘空间;
特殊情况比如今天中午后端发布了一个新版本,数据会有变化,前端只需修改版本号重新部署,脚本会自动删除旧的缓存名,并生成新的缓存名。
对于相同的请求参数,会优先从缓存中找,找不到会发请求,并将请求结果保存到缓存中以便下次使用。
不是所有请求都适合使用缓存,比如修改接口,新增接口就不要用缓存,只有查询接口使用缓存才有意义。下
缓存名和缓存键之间的关系:
|-- 缓存名 <- 相当于目录
|-- 缓存键:缓存值 <- 相当于文件
|-- 缓存键:缓存值 <- 相当于文件
|-- 缓存键:缓存值 <- 相当于文件
脚本示例:
// 添加需要使用缓存的查询接口
const whitelist = [
'/users',
'/roles',
// ....
]
// 版本号,如果需要设置缓存失效,更改版本号重新部署,用户再下一次刷新页面时旧缓存就会失效
const APP_VERSION = 'v1.0.0'
// 缓存名前缀
const CACHE_NAME_PREFIX = 'api-cache'
// 获取今天的日期字符串 (YYYY-MM-DD)
function getTodayDateStr() {
const now = new Date()
return now.toISOString().split('T')[0] // 例如 "2026-01-07"
}
// 生成请求的唯一缓存键
async function getCacheKey(request) {
const url = new URL(request.url);
const keyParts = [
request.method,
url.origin,
url.pathname,
url.search,
]
// 对 Content-Type 为 application/json 的情况做处理
if (request.headers.get('Content-Type')?.includes('application/json')) {
try {
const cloneReq = request.clone();
const body = await cloneReq.text();
if (body) {
keyParts.push(body);
}
} catch (e) {
console.warn('Could not read request body for cache key:', e);
}
}
// 加入日期和版本,实现按天和按版本隔离缓存
keyParts.push(getTodayDateStr(), APP_VERSION);
return keyParts.join('|');
}
// 获取或创建以“当天日期+版本”命名的缓存
async function getTodayCache() {
const cacheName = `${CACHE_NAME_PREFIX}-${getTodayDateStr()}-${APP_VERSION}`;
return await caches.open(cacheName);
}
// 安装阶段:预缓存关键静态资源(可选)
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
self.skipWaiting(); // 强制激活新的 SW
})
// 激活阶段:清理过期的旧缓存(非今天且非当前版本的缓存)
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
const validCachePrefixes = [`${CACHE_NAME_PREFIX}-${getTodayDateStr()}-${APP_VERSION}`];
return Promise.all(
cacheNames.map((cacheName) => {
// 删除所有不符合“当天日期+当前版本”的 api-cache 缓存
if (cacheName.startsWith(CACHE_NAME_PREFIX) && !validCachePrefixes.some(prefix => cacheName.startsWith(prefix))) {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// 立即控制所有客户端页面
self.clients.claim();
})
// 拦截 fetch 事件,处理 API 请求缓存
self.addEventListener('fetch', (event) => {
const request = event.request;
const url = new URL(request.url);
// 只拦截指向我们 API 的白名单请求
if (whitelist.includes(url.pathname)) {
event.respondWith(
(async () => {
try {
const cacheKey = await getCacheKey(request);
const todayCache = await getTodayCache();
const cachedResponse = await todayCache.match(cacheKey);
// 1. 尝试从缓存中获取响应
if (cachedResponse) {
console.log(`[Service Worker] Cache HIT for: ${request.url}`);
// 在返回缓存的同时,发起网络请求以更新缓存(Stale-While-Revalidate)
event.waitUntil(
(async () => {
try {
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.status === 200) {
const responseClone = networkResponse.clone();
await todayCache.put(cacheKey, responseClone);
console.log(`[Service Worker] Cache UPDATED for: ${request.url}`);
}
} catch (err) {
console.warn(`[Service Worker] Failed to update cache for ${request.url}:`, err);
// 网络更新失败,保留旧缓存
}
})()
);
return cachedResponse;
}
// 2. 缓存中没有,发起网络请求
console.log(`[Service Worker] Cache MISS for: ${request.url}`);
const networkResponse = await fetch(request);
// 只缓存成功的响应(状态码 200-299)
if (networkResponse && networkResponse.ok) {
const responseClone = networkResponse.clone();
await todayCache.put(cacheKey, responseClone);
console.log(`[Service Worker] Cached NEW response for: ${request.url}`);
}
return networkResponse;
} catch (error) {
// 3. 网络请求失败,且无缓存,返回一个兜底响应或错误
console.error('[Service Worker] Fetch failed:', error, request.url);
// 可以在这里返回一个预设的离线响应,例如:
return new Response(JSON.stringify({ error: 'Network error and no cache available' }), {
status: 503,
headers: { 'Content-Type': 'application/json' },
});
}
})()
);
}
});
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
// 注意:sw.js 的路径是相对于域名的根目录。Umi 构建后,sw.js 会在 dist 根目录。
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('ServiceWorker 注册成功,作用域: ', registration.scope);
// 监听 Service Worker 的更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 此时有新的 Service Worker 已安装,等待激活。
// 可以在这里提示用户“有新版本可用,请刷新页面”。
// if (confirm('发现新版本,是否立即刷新以应用更新?')) {
// window.location.reload();
// }
console.log('发现新版本');
}
});
});
})
.catch((err) => {
console.error('ServiceWorker 注册失败: ', err);
});
});
}
↶ 返回首页 ↶