js 在前端实现缓存 api 响应数据

2026-01-07 12:49:40

问题

工作中开发的网站,请求一次要很长时间,对于查询条件相同的请求,期望使用上一次的响应结果,不再重发请求。

之前的做法是每个接口单独处理,将 api 查询参数和响应结果存在变量里,每次请求时手动判断查询参数是否变化,如果没有变化,使用存在变量里的缓存数据,否则发送请求。

经过调研,发现浏览器有两个 API 可以更优雅地实现 api 数据缓存: Service worker + Cache。这个缓存是存在硬盘里的,不会导致网页内存暴增,刷新页面也不会丢失缓存。(之前存在变量里,刷新页面缓存会丢失)

实现

创建 sw.js,复制如下脚本内容,在入口文件中引入 sw.js 即可。

大概介绍下脚本:

  1. 缓存名由三部分组成: 缓存前缀(CACHE_NAME_PREFIX) + 日期(当天,如: 2026-01-07) + 版本号(APP_VERSION)。

默认情况下,第二天会删除前一天的缓存,保证浏览器缓存数据不会一直增长,消耗硬盘空间;

特殊情况比如今天中午后端发布了一个新版本,数据会有变化,前端只需修改版本号重新部署,脚本会自动删除旧的缓存名,并生成新的缓存名。

  1. 缓存键: 请求URL + 请求方法 + 请求参数 + 日期(当天,如: 2026-01-07) + 版本号(APP_VERSION)

对于相同的请求参数,会优先从缓存中找,找不到会发请求,并将请求结果保存到缓存中以便下次使用。

  1. 白名单:whitelist

不是所有请求都适合使用缓存,比如修改接口,新增接口就不要用缓存,只有查询接口使用缓存才有意义。下

缓存名和缓存键之间的关系:

|-- 缓存名          <- 相当于目录
    |-- 缓存键:缓存值       <- 相当于文件
    |-- 缓存键:缓存值       <- 相当于文件
    |-- 缓存键:缓存值       <- 相当于文件

脚本示例:

// 添加需要使用缓存的查询接口
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);
            });
    });
}

返回首页

本文总阅读量  次
皖ICP备17026209号-3
总访问量: 
总访客量: