高频接口前端缓存解决方案:Service Worker + IndexedDB

22 阅读3分钟

背景

在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 场景下的高效缓存。既提升了用户体验,也减轻了后端压力,适用于大部分前端高频数据场景,实际项目可根据接口特性灵活调整缓存策略。