前端数据缓存方案
pfan123 opened this issue · comments
缓存一直以来作为性能优化的一环被广泛使用,
- 数据库缓存
- 代理服务器缓存
- CDN 缓存
- 浏览器缓存
等等,几乎在每一层,都有缓存存在。本篇博客讨论的不是上面这些缓存,而是由我们自己控制的缓存,具体来说是「请求」的缓存,如何优化请求的缓存让我们的应用更好。
一、需要缓存吗?
1、减少不必要的请求
在我们的应用中,会存在一些很少改动的数据,但这些数据又要从后端获取。
典型的如下拉框的内容,可以是行业、职业、角色等等,这类数据在很长一段时间内都是不会改变的,至少在应用的使用过程中是不会改变的,而我们却在每次打开页面或者是切换页面时,就重新向后端请求了一次,这类请求完全是没有必要的,通过对类请求的数据进行缓存,可以大大减小对服务器的压力。
2、更快的访问速度
在访问一些数据时,不再重新向后端请求,而是直接使用缓存中的数据,访问速度毫无疑问会更加快,用户体验也必然会更好。
二、哪些数据可以缓存?
在单页面应用中,所有数据来源都是接口请求,但并不是所有数据都需要,或者说能被缓存。
讨论的都是
GET
请求,PUT
、POST
等绝对是不能被缓存的。
判断标准是根据请求的频次,这里给出不同请求频次的定义,「高频」、「中频」、「低频」。
- 高频:通过交互就可以请求的数据,如查询接口
- 中频:页面切换时才会请求的数据,如页面数据
- 低频:只有在应用初始化时才会请求,如获取当前登录用户信息
具体的可以根据自己项目进行调整。
举个例子,页面展示「图书列表」,可以对该列表进行查询、切换页码、删除。根据上面的定义得出如下判断
- 1、类别下拉框,只在访问页面时请求,用户交互不会触发重新请求,所以属于「中频」。
- 2、获取图书列表,可以通过切换分页请求,所以属于「高频」。
- 3、查询功能,同样可以通过点击请求,所以属于「高频」。
- 4、当前登录用户名,在单页面应用中切换页面也不会重新请求,所以属于「低频」。
- 5、删除功能,不缓存。
在确定请求频次后,还要判断「该数据是否会被修改」,假设我们认为「获取图书列表」是高频所以缓存,确实能解决用户切换分页时频繁请求数据的问题,但如果用户删除了某条记录,在切换分页后会发现该数据还存在。
所以当数据是可以被新增、删除、修改时,就不能缓存该数据了。
或许可以设置一种机制,当接口存在这三种请求,缓存即过期,将重新请求。
三、内存还是 localstorage ?
在确定了哪些数据需要缓存后,如何将数据缓存呢?
由于要使用,当然保存在一个全局变量会方便很多,也就是保存在内存中。如果说保存在localStorage
,每次使用时还是要读取到内存中,那干脆都保存到内存,localStorage
作为持久化方案,保存一些低频、不会变化的数据,如「用户信息」,如果有的话。
四、具体代码如何写
能否实现对已有系统改造量最小,甚至做到无需修改呢?
在目前普通使用redux
的情况下,在「请求数据」这里做比较好,一开始想到,一般我们使用axios
请求数据,当请求需要缓存的接口时,就直接返回缓存好的,通过拦截器可以轻松做到。
但问题来了,如何标志哪些接口是需要缓存,哪些又是不需要的呢,通过method
判断可以解决一部分,但还是不够完善。
要解决这个问题,确实应该在请求前就处理好,如果不使用拦截器,我们可以自己做一次处理,以具体代码来说:
// api.js
export function fetch(params) {
return axios.get('/api/books', { params });
}
export function fetchCategories() {
return axios.get('/api/categories');
}
export function delete(id) {
return axios.delete(`/api/books/${id}`);
}
简单粗暴的做法,直接加一个缓存对象,一旦请求,就加入缓存,否则就请求。
const cache = {};
export function fetchCategories() {
const url = '/api/categories';
if (cache[url]) {
return cache[url];
}
const res = axios.get(url);
cache[url] = res;
return res;
}
虽然简单粗暴,但这是核心逻辑,能够优化的就是如何优雅的写代码了。
1、现成的缓存库
这种需求肯定早有人想过,先来看一个已有的缓存库 mem(适合单页面使用,缓存cache是保存在内存里面,而不是sessionStorage、localStorage),
const mem = require('mem');
let i = 0;
const counter = async () => ++i;
const memoized = mem(counter);
(async () => {
console.log(await memoized());
//=> 1
// The return value didn't increase as it's cached
console.log(await memoized());
//=> 1
})();
counter
就是要被缓存的请求,第二次调用时,会返回之前的值,而不会再次调用该请求。
换成我们的代码,就是这样:
const mem = require('mem');
export const fetchCategories = mem(function() {
return axios.get('/api/categories');
});
2、mem 在实际项目中的拓展
如果需求比较简单,目前应该就能够满足需求了。
但其实还可以更一步优化,举例来说,虽然上面提到「图书列表」会被删除修改,所以不应该缓存,但如果用户在页码之间来回切换,请求的频率还是很高的,而且这种情况下是完全可以缓存的,所以,判断两次请求的时间间隔,如果小于 5s,就返回缓存的结果,否则就不缓存。
当然这种需求mem
的作者也考虑到了,就是过期时间,
const mem = require('mem');
export const fetchCategories = mem(function() {
return axios.get('/api/categories');
}, {
maxAge: 5000,
});
表示设置缓存有效期是 5s,5s 内多次请求,都会返回缓存,5s 后会重新请求。
上面写不太直观,我们可以使用修饰器来简化,但这种方式对原有代码调整很大,因为装饰器只能用于类与类的方法,所以代码变成这样:
import mem from 'mem';
/**
* @param {MemOption} - mem 配置项
* @return {Function} - 装饰器
*/
function m(options) {
return (target, name, descriptor) => {
const oldValue = descriptor.value;
descriptor.value = mem(oldValue, options);
return descriptor;
};
}
class Api {
@m({ maxAge: 5000 })
fetchCategories() {
return axios.get('/api/categories');
}
}