您的位置:首页 > 移动开发 > IOS开发

利用LRU策略实现Axios请求缓存

2021-08-01 17:01 405 查看

业务场景

前一段时间刚做完一个项目,先说一下业务场景,有别于其他的前端项目,这次的项目是直接调用第三方服务的接口,而我们的服务端只做鉴权和透传,第三方为了灵活,把接口拆的很零散,所以这个项目就像扔给你一堆乐高颗粒让你组装成一个机器人。所以可以大概分析一下这个项目在请求接口时的一些特点,然后针对性的做一些优化:

  1. 请求接口多,可能你的一个n个条目的列表本来一个接口搞定现在需要n*10个接口才能拿到完整的数据,有些功能模块可能需要请求成千上万次接口;
  2. 基本都是get请求,只读不写;
  3. 接口调用重复率高,因为接口很细碎,所以可能有些常用的接口需要重复调用;
  4. 接口返回的数据实时性要求不高,第三方的数据不是实时更新的,可能一天或者一周才更新一次,但是第三方要求不能以任何的方式落库。

所以综上分析,前端缓存成了一个可行性较高的优化方案。

解决方案

前端的HTTP请求使用的是Axios,因此可以利用Axios的拦截器进行缓存的管理。梳理一下逻辑:

  1. 创建缓存对象;
  2. 请求发起之前判断该请求是否命中缓存: 是,直接返回缓存内容;
  3. 否,发起请求,请求成功后将请求结果存入缓存中。

如标题所说,这里的缓存策略我们用的是LRU(Least Recently Used)策略,因为缓存不能无限大,过大的缓存可能会导致浏览器页面性能下降,甚至内存泄漏。LRU会在缓存达到最大承载量后删除最近最少使用的缓存内容,因此不用担心缓存无限增大。那么如何实现LRU缓存策略呢?Github上有现成的轮子,但是为了更深入的学习嘛,我们自己来手动实现一个。

实现LRU

LRU主要有两个功能,存、取。梳理一下逻辑:

  1. 存入: 如果缓存已满,删除最近最少使用的缓存内容,把当前的缓存存进去,放到最常用的位置;
  2. 否则直接将缓存存入最常用的位置。
  • 读取:
      如果存在这个缓存,返回缓存内容,同时把该缓存放到最常用的位置;
    1. 如果没有,返回-1。

    这里我们可以看到,缓存是有优先级的,我们用什么来标明优先级呢?如果用数组存储可以将不常用的放到数组的头部,将常用的放到尾部。但是鉴于数据的插入效率不高,这里我们使用Map对象来作为容器存储缓存。

    代码如下:

    class LRUCache {
    constructor(capacity) {
    if (typeof capacity !== 'number' || capacity < 0) {
    throw new TypeError('capacity必须是一个非负数');
    }
    this.capacity = capacity;
    this.cache = new Map();
    }
    
    get(key) {
    if (!this.cache.has(key)) {
    return -1;
    }
    let tmp = this.cache.get(key);
    // 将当前的缓存移动到最常用的位置
    this.cache.delete(key);
    this.cache.set(key, tmp);
    return tmp;
    }
    
    set(key, value) {
    if (this.cache.has(key)) {
    // 如果缓存存在更新缓存位置
    this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
    // 如果缓存容量已满,删除最近最少使用的缓存
    this.cache.delete(this.cache.keys().next.val);
    }
    this.cache.set(key, value);
    }
    }

    结合Axios实现请求缓存

    理一下大概的逻辑:每次请求根据请求的方法、url、参数生成一串hash,缓存内容为hash->response,后续请求如果请求方法、url、参数一致,即认为命中缓存。

    代码如下:

    import axios from 'axios';
    import md5 from 'md5';
    import LRUCache from './LRU.js';
    
    const cache = new LRUCache(100);
    
    const _axios = axios.create();
    
    // 将请求参数排序,防止相同参数生成的hash不同
    function sortObject(obj = {}) {
    let result = {};
    Object.keys(obj)
    .sort()
    .forEach((key) => {
    result[key] = obj[key];
    });
    }
    
    // 根据request method,url,data/params生成cache的标识
    function genHashByConfig(config) {
    const target = {
    method: config.method,
    url: config.url,
    params: config.method === 'get' ? sortObject(config.params) : null,
    data: config.method === 'post' ? sortObject(config.data) : null,
    };
    return md5(JSON.stringify(target));
    }
    
    _axios.interceptors.response.use(
    function(response) {
    // 设置缓存
    const hashKey = genHashByConfig(response.config);
    cache.set(hashKey, response.data);
    return response.data;
    },
    function(error) {
    return Promise.reject(error);
    }
    );
    
    // 将axios请求封装,如果命中缓存就不需要发起http请求,直接返回缓存内容
    export default function request({
    method,
    url,
    params = null,
    data = null,
    ...res
    }) {
    const hashKey = genHashByConfig({ method, url, params, data });
    const result = cache.get(hashKey);
    if (~result) {
    console.log('cache hit');
    return Promise.resolve(result);
    }
    return _axios({ method, url, params, data, ...res });
    }

    请求的封装:

    import request from './axios.js';
    
    export function getApi(params) {
    return request({
    method: 'get',
    url: '/list',
    params,
    });
    }
    
    export function postApi(data) {
    return request({
    method: 'post',
    url: '/list',
    data,
    });
    }

    这里需要注意的一点是,我将请求方法,url,参数进行了hash操作,为了防止参数的顺序改变而导致hash结果不一致,我在hash操作之前,给参数做了排序处理,实际开发中,参数的类型也不一定就是object,可以根据自己的需求进行改造。

    如上改造后,第一次请求后,相同的请求再次触发就不会发送http请求了,而是直接从缓存中获取,真是多快好省~

    参考:JS 实现一个 LRU 算法

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: