利用LRU策略實現Axios請求緩存


業務場景

前一段時間剛做完一個項目,先說一下業務場景,有別於其他的前端項目,這次的項目是直接調用第三方服務的接口,而我們的服務端只做鑒權和透傳,第三方為了靈活,把接口拆的很零散,所以這個項目就像扔給你一堆樂高顆粒讓你組裝成一個機器人。所以可以大概分析一下這個項目在請求接口時的一些特點,然后針對性的做一些優化:

  1. 請求接口多,可能你的一個n個條目的列表本來一個接口搞定現在需要n*10個接口才能拿到完整的數據,有些功能模塊可能需要請求成千上萬次接口;
  2. 基本都是get請求,只讀不寫;
  3. 接口調用重復率高,因為接口很細碎,所以可能有些常用的接口需要重復調用;
  4. 接口返回的數據實時性要求不高,第三方的數據不是實時更新的,可能一天或者一周才更新一次,但是第三方要求不能以任何的方式落庫。

所以綜上分析,前端緩存成了一個可行性較高的優化方案。

解決方案

前端的HTTP請求使用的是Axios,因此可以利用Axios的攔截器進行緩存的管理。梳理一下邏輯:

  1. 創建緩存對象;
  2. 請求發起之前判斷該請求是否命中緩存:
    1. 是,直接返回緩存內容;
    2. 否,發起請求,請求成功后將請求結果存入緩存中。

如標題所說,這里的緩存策略我們用的是LRU(Least Recently Used)策略,因為緩存不能無限大,過大的緩存可能會導致瀏覽器頁面性能下降,甚至內存泄漏。LRU會在緩存達到最大承載量后刪除最近最少使用的緩存內容,因此不用擔心緩存無限增大。那么如何實現LRU緩存策略呢?Github上有現成的輪子,但是為了更深入的學習嘛,我們自己來手動實現一個。

實現LRU

LRU主要有兩個功能,存、取。梳理一下邏輯:

  1. 存入:
    1. 如果緩存已滿,刪除最近最少使用的緩存內容,把當前的緩存存進去,放到最常用的位置;
    2. 否則直接將緩存存入最常用的位置。
  2. 讀取:
    1. 如果存在這個緩存,返回緩存內容,同時把該緩存放到最常用的位置;
    2. 如果沒有,返回-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 算法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM