前端接口请求缓存机制实现
核心需求: 为指定的 API 请求增加一个持续数分钟的缓存。目的是为了优化那些后端无法再提速的慢接口,通过在前端缓存数据,减少重复请求,提升用户体验。
1. 核心挑战与思考
在实现该功能时,需要考虑以下三个关键问题:
无感封装 (Seamless Encapsulation):
- 缓存机制应该对开发者透明。开发者只需像平常一样调用请求方法,无需关心该请求是否应该使用缓存。缓存逻辑应被完全封装。
自动过期 (Automatic Expiration):
- 缓存需要在指定时间(如3-5分钟)后自动失效。
- 如何高效实现?如果为每个缓存都设置一个独立的
setTimeout定时器,当请求数量多时会造成严重的性能问题。
储存位置 (Storage Location):
- 缓存数据应该存储在哪里?
localStorage还是内存中的一个 JavaScript 对象? - 不同存储方式的优劣势是什么?
- 缓存数据应该存储在哪里?
2. 解决方案思路
2.1. 整体流程
- 封装一个统一的请求方法(如
request.get,request.post)。 - 当调用该方法时,首先检查是否存在针对该请求的本地缓存。
- 如果存在缓存:
- 检查该缓存是否已过期(通过比较当前时间与缓存时记录的过期时间戳)。
- 未过期:直接返回缓存数据。
- 已过期:删除旧缓存,发起真实的 API 请求。
- 如果不存在缓存:
- 直接发起真实的 API 请求。
- 当真实的 API 请求成功返回后,将返回结果连同计算出的 过期时间戳 一同存入缓存中。
2.2. 缓存数据结构设计
为了实现高效的过期判断,我们不使用定时器,而是在存储数据时附加一个过期时间戳。
- 键 (Key): 使用接口的 URL 地址作为唯一的键。
- 值 (Value): 存储一个包含
接口返回数据和expire(过期时间戳) 的对象。
// cacheStore 的示例结构
{
// 使用请求 URL 作为属性名
"/api/order/detail?id=1": {
// ...这里是 Axios 返回的原始 res 对象的全部内容
"data": { "id": 1, "product": "商品A", "price": 100 },
"status": 200,
"config": { ... },
// ...其他 res 属性
// 额外添加一个过期时间字段
"expire": 1721900400000 // 例如:当前时间 + 5分钟后的毫秒时间戳
}
}核心优势:每次使用缓存前,只需用当前时间戳与 expire 字段进行比较,即可判断缓存是否有效,避免了使用大量定时器带来的性能开销。
3. 核心代码实现
以下是一个基于 axios 的实现示例。
3.1. 缓存存储策略 (cacheStore)
我们选择将缓存存储在模块内的 JavaScript 对象中,而不是 localStorage。
// request.js
// 使用一个普通对象作为内存缓存的容器
const cacheStore = {};- 优点: 当用户刷新页面时,
cacheStore对象会被重新初始化为空对象{},所有缓存自动清除。这适用于希望刷新页面就能获取最新数据的场景。 - 缺点: 缓存无法跨页面会话持久化。
- 替代方案: 如果需要持久化缓存(即使刷新也不清除),可以将
cacheStore的操作替换为对localStorage的setItem和getItem操作。
3.2. 响应拦截器:自动缓存数据
利用 axios 的响应拦截器,可以在每次请求成功后自动将数据存入缓存。
// request.js
import axios from 'axios';
const CACHE_DURATION = 5 * 60 * 1000; // 缓存有效期:5分钟
const service = axios.create({
baseURL: '/api',
timeout: 5000,
});
// 响应拦截器
service.interceptors.response.use(res => {
// --- 核心缓存逻辑 ---
const key = res.config.url; // 使用请求 URL 作为 key
if (key) {
const expire = Date.now() + CACHE_DURATION; // 计算过期时间戳
cacheStore[key] = { ...res, expire }; // 将原始响应和过期时间一起存入缓存
}
return res;
});设计亮点 (抹平差异): 使用 ...res 将整个响应对象展开存入缓存,而不是 data: res。这是为了确保无论数据来自缓存还是真实请求,其数据结构完全一致,调用者无需编写额外的判断代码来处理不同层级的数据。
3.3. 请求方法封装:无感使用缓存
我们导出的不是 axios 实例本身,而是一个包含了 get 和 post 方法的对象,在这些方法内部实现了缓存的检查逻辑。
// request.js (续)
/**
* 检查并返回有效缓存
* @param {string} key - 缓存的键 (URL)
* @returns {object|false} - 如果存在且未过期,返回缓存对象;否则返回 false
*/
function hasCache(key) {
if (cacheStore[key]) {
const cache = cacheStore[key];
// 检查时间戳
if (Date.now() < cache.expire) {
console.log('✅ 命中缓存:', key);
return cache; // 缓存有效
} else {
// 缓存已过期,从 store 中删除
delete cacheStore[key];
}
}
return false; // 无有效缓存
}
// 导出的请求对象
export default {
get(url, config) {
const key = url; // 对于 GET 请求,URL 本身就是唯一标识
const cache = hasCache(key);
if (cache) {
// 命中缓存,直接返回一个 resolved 的 Promise,模拟 axios 的行为
return Promise.resolve(cache);
}
// 未命中缓存,发起真实请求
return service.get(url, config);
},
post(url, data, config) {
// 对于 POST,需要将 data 也作为 key 的一部分
const key = url + JSON.stringify(data);
const cache = hasCache(key);
if (cache) {
return Promise.resolve(cache);
}
return service.post(url, data, config);
}
};设计亮点 (Promise.resolve): 当命中缓存时,使用 Promise.resolve(cache) 将普通对象包装成一个 Promise。这样做是为了与 axios 的异步行为保持一致,让开发者可以始终使用 .then() 或 await 来处理返回结果,而无需关心数据来源。
4. 进阶问题与优化
4.1. 处理带参数的接口
GET 请求: 参数通常拼接在 URL 中(如
?id=1)。由于我们将整个 URL 作为key,因此不同参数的 GET 请求会被视为不同的缓存,已天然支持。POST 请求: 参数在请求体(
data)中,不在 URL 里。- 解决方案: 在生成
key时,将URL和请求体data一起组合成一个唯一的字符串。
javascript// 在 post 方法中 const key = url + JSON.stringify(data); // 在响应拦截器中,也需要同样处理 service.interceptors.response.use(res => { let key = res.config.url; if (res.config.method.toLowerCase() === 'post') { key += JSON.stringify(JSON.parse(res.config.data)); // axios 的 data 是 string, 需要先 parse } // ...后续缓存逻辑 });- 解决方案: 在生成
4.2. 缓存数量与内存管理
- 问题: 如果在短时间内请求大量不同的接口,
cacheStore对象会持续增大,可能导致内存压力。 - 解决方案: 实现一个缓存淘汰策略,限制缓存的最大数量。例如,采用 先进先出(FIFO) 策略。
- 维护一个记录了所有
key的数组。 - 在存入新缓存前,检查
cacheStore的大小。 - 如果超出预设阈值(如 20 个),则从
key数组的头部取出一个最老的key,并从cacheStore中删除对应项,然后再存入新的缓存。
- 维护一个记录了所有
4.3. 浏览器缓存 vs. 应用层缓存
- 常见疑问: 为什么不使用浏览器自带的 HTTP 缓存(如
Cache-Control: max-age)? - 解答: 浏览器 HTTP 缓存主要用于缓存静态资源文件(如 JS、CSS、图片)。对于通过
XMLHttpRequest或fetch发起的动态数据接口请求,浏览器通常不会自动缓存其返回的 JSON 数据。因此,我们需要在应用层面手动实现这种针对 API 数据的缓存机制。