pfan123 / Articles

经验文章

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前端数据缓存方案

pfan123 opened this issue · comments

缓存一直以来作为性能优化的一环被广泛使用,

  • 数据库缓存
  • 代理服务器缓存
  • CDN 缓存
  • 浏览器缓存

等等,几乎在每一层,都有缓存存在。本篇博客讨论的不是上面这些缓存,而是由我们自己控制的缓存,具体来说是「请求」的缓存,如何优化请求的缓存让我们的应用更好。

一、需要缓存吗?

1、减少不必要的请求

在我们的应用中,会存在一些很少改动的数据,但这些数据又要从后端获取。
典型的如下拉框的内容,可以是行业、职业、角色等等,这类数据在很长一段时间内都是不会改变的,至少在应用的使用过程中是不会改变的,而我们却在每次打开页面或者是切换页面时,就重新向后端请求了一次,这类请求完全是没有必要的,通过对类请求的数据进行缓存,可以大大减小对服务器的压力。

2、更快的访问速度

在访问一些数据时,不再重新向后端请求,而是直接使用缓存中的数据,访问速度毫无疑问会更加快,用户体验也必然会更好。

二、哪些数据可以缓存?

在单页面应用中,所有数据来源都是接口请求,但并不是所有数据都需要,或者说能被缓存。

讨论的都是GET请求,PUTPOST等绝对是不能被缓存的。

判断标准是根据请求的频次,这里给出不同请求频次的定义,「高频」、「中频」、「低频」。

  • 高频:通过交互就可以请求的数据,如查询接口
  • 中频:页面切换时才会请求的数据,如页面数据
  • 低频:只有在应用初始化时才会请求,如获取当前登录用户信息

具体的可以根据自己项目进行调整。

举个例子,页面展示「图书列表」,可以对该列表进行查询、切换页码、删除。根据上面的定义得出如下判断

  • 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');
    }
}

五、参考