1. 重复请求缓存的核心价值
前端开发中最让人头疼的问题之一就是重复请求——当用户频繁点击某个按钮或快速切换页面时,系统往往会向服务器发送大量相同请求。这不仅浪费带宽,还会导致服务器压力激增,最终影响用户体验。我在电商项目中就遇到过这样的场景:促销活动期间,商品详情页的"立即购买"按钮被疯狂点击,导致下单接口每秒被调用上百次,服务器直接崩溃。
重复请求缓存的本质,是在客户端(浏览器或移动端)建立一道"防洪堤",对相同请求进行智能拦截和复用。举个例子:当用户连续点击"查询订单"按钮时,只有第一次点击会真实发送请求,后续点击在缓存有效期内都会直接返回第一次的结果。这种机制能降低80%以上的冗余请求量,对高并发场景尤为有效。
2. 缓存策略深度解析
2.1 浏览器原生缓存控制
通过Request API的cache属性,我们可以精细控制请求与浏览器缓存的交互方式:
// 强制绕过缓存,适合实时性要求极高的场景 fetch('/api/data', { cache: 'no-store' }) // 优先使用缓存(包括过期缓存),适合展示型数据 fetch('/api/news', { cache: 'force-cache' }) // 典型用法:先验证缓存有效性再决定是否请求 fetch('/api/user', { cache: 'no-cache', headers: { 'If-None-Match': etagValue } })但原生缓存存在明显局限:
- 无法针对业务逻辑定制(如只缓存POST请求的特定参数组合)
- 缓存时间不可编程控制
- 不同浏览器实现差异大
2.2 内存级缓存方案
对于需要精细控制的场景,推荐使用内存缓存。以下是基于Map的简易实现:
class RequestCache { constructor(ttl = 3000) { this.cache = new Map() this.ttl = ttl } async get(key, fetcher) { if (this.cache.has(key)) { const { data, timestamp } = this.cache.get(key) if (Date.now() - timestamp < this.ttl) { return data } } const freshData = await fetcher() this.cache.set(key, { data: freshData, timestamp: Date.now() }) return freshData } } // 使用示例 const cache = new RequestCache() const fetchUser = () => axios.get('/user/123') cache.get('user_123', fetchUser) // 实际发送请求 cache.get('user_123', fetchUser) // 3秒内返回缓存结果2.3 持久化缓存进阶
对于SPA应用,结合localStorage可以实现页面刷新不丢失的缓存:
class PersistentCache { constructor(namespace) { this.namespace = namespace } async get(key, fetcher) { const cached = localStorage.getItem(`${this.namespace}:${key}`) if (cached) { const { data, expire } = JSON.parse(cached) if (expire > Date.now()) { return data } } const freshData = await fetcher() localStorage.setItem( `${this.namespace}:${key}`, JSON.stringify({ data: freshData, expire: Date.now() + 3600000 // 1小时过期 }) ) return freshData } }3. 实战中的缓存策略
3.1 请求指纹生成
精准识别重复请求的关键是生成唯一请求指纹。推荐以下算法:
function generateRequestKey(config) { return [ config.method, config.url, JSON.stringify(config.params), JSON.stringify(config.data) ].join('|') } // 示例:GET /api/users?page=1 和 GET /api/users?page=2 会被识别为不同请求3.2 缓存失效策略
常见的三种失效策略对比:
| 策略类型 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 定时过期 | 设置固定TTL | 数据变化频率固定 | 实现简单,但可能过早失效 |
| 主动清除 | 提供clearCache方法 | 关键数据更新后 | 精准控制,但需手动触发 |
| 版本号控制 | 在URL中添加v=1.0参数 | 静态资源 | 一劳永逸,但需构建配合 |
3.3 防抖与缓存的结合
对于高频触发场景(如搜索框输入),建议结合防抖技术:
const searchCache = new RequestCache() const debounceSearch = _.debounce(async (keyword) => { return searchCache.get(`search_${keyword}`, () => { return axios.get('/search', { params: { q: keyword } }) }) }, 300)4. 性能优化实测数据
在电商项目中的实测对比(单位:ms):
| 场景 | 无缓存 | 内存缓存 | 本地存储缓存 |
|---|---|---|---|
| 首屏加载 | 1200 | 1200 | 800 |
| 二次访问 | 1100 | 50 | 20 |
| 网络断开时 | 失败 | 失败 | 可用 |
| 内存占用 | 低 | 中 | 高 |
5. 常见问题解决方案
5.1 缓存雪崩预防
当大量缓存同时失效时,会导致请求直接打到后端。解决方案:
// 在基础TTL上增加随机抖动 const ttl = 3000 + Math.random() * 20005.2 敏感数据隔离
对于不同安全等级的数据,应该使用独立的缓存实例:
const publicCache = new RequestCache() // 公共数据 const privateCache = new RequestCache() // 用户私有数据5.3 移动端优化技巧
在React Native中,建议将缓存与AsyncStorage结合:
import { AsyncStorage } from 'react-native' class RNRequestCache { async get(key, fetcher) { try { const cached = await AsyncStorage.getItem(key) if (cached) { /* 处理逻辑 */ } } catch (e) { // 存储失败时降级为普通请求 return fetcher() } } }6. 高级应用场景
6.1 GraphQL请求缓存
针对GraphQL的特性,需要特殊处理查询指纹:
function generateGraphQLKey(query, variables) { return hashString(JSON.stringify({ query: minifyQuery(query), variables })) }6.2 文件上传缓存
对大文件上传实现断点续传:
const uploadCache = { setProgress(fileHash, progress) { localStorage.setItem(`upload_${fileHash}`, progress) }, getProgress(fileHash) { return localStorage.getItem(`upload_${fileHash}`) } }6.3 Service Worker集成
通过workbox实现更底层的缓存控制:
import { CacheFirst } from 'workbox-strategies' registerRoute( '/api/products', new CacheFirst({ cacheName: 'api-cache', plugins: [/* 自定义插件 */] }) )在实现缓存方案时,一定要根据业务特点选择合适策略。我曾在一个金融项目中过度依赖本地缓存,导致用户看到陈旧的账户余额。最终我们采用分层缓存方案:基础信息缓存1小时,资金相关数据强制实时请求。记住:缓存是手段而非目的,永远要以业务正确性为前提。