Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端数据缓存方案 #33

Open
pfan123 opened this issue Apr 2, 2019 · 0 comments
Open

前端数据缓存方案 #33

pfan123 opened this issue Apr 2, 2019 · 0 comments

Comments

@pfan123
Copy link
Owner

pfan123 commented Apr 2, 2019

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

  • 数据库缓存
  • 代理服务器缓存
  • 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');
    }
}

五、参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant