業務場景
前一段時間剛做完一個專案,先說一下業務場景,有別於其他的前端專案,這次的專案是直接呼叫第三方服務的介面,而我們的服務端只做鑑權和透傳,第三方為了靈活,把介面拆的很零散,所以這個專案就像扔給你一堆樂高顆粒讓你組裝成一個機器人。所以可以大概分析一下這個專案在請求介面時的一些特點,然後針對性的做一些優化:
- 請求介面多,可能你的一個n個條目的列表本來一個介面搞定現在需要n*10個介面才能拿到完整的資料,有些功能模組可能需要請求成千上萬次介面;
- 基本都是get請求,只讀不寫;
- 介面呼叫重複率高,因為介面很細碎,所以可能有些常用的介面需要重複呼叫;
- 介面返回的資料實時性要求不高,第三方的資料不是實時更新的,可能一天或者一週才更新一次,但是第三方要求不能以任何的方式落庫。
所以綜上分析,前端快取成了一個可行性較高的優化方案。
解決方案
前端的HTTP請求使用的是Axios,因此可以利用Axios的攔截器進行快取的管理。梳理一下邏輯:
- 建立快取物件;
- 請求發起之前判斷該請求是否命中快取:
- 是,直接返回快取內容;
- 否,發起請求,請求成功後將請求結果存入快取中。
如標題所說,這裡的快取策略我們用的是LRU(Least Recently Used)策略,因為快取不能無限大,過大的快取可能會導致瀏覽器頁面效能下降,甚至記憶體洩漏。LRU會在快取達到最大承載量後刪除最近最少使用的快取內容,因此不用擔心快取無限增大。那麼如何實現LRU快取策略呢?Github上有現成的輪子,但是為了更深入的學習嘛,我們自己來手動實現一個。
實現LRU
LRU主要有兩個功能,存、取。梳理一下邏輯:
- 存入:
- 如果快取已滿,刪除最近最少使用的快取內容,把當前的快取存進去,放到最常用的位置;
- 否則直接將快取存入最常用的位置。
- 讀取:
- 如果存在這個快取,返回快取內容,同時把該快取放到最常用的位置;
- 如果沒有,返回-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請求了,而是直接從快取中獲取,真是多快好省~