210 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
isObject, isString, isURLSearchParams, checkHttpRequestHasBody
} from './helpers';
/**
* 缓存实现的功能
* 1. 唯一定位一个请求url, data | params, method
* 其中请求参数根据请求方法使用其中一个就够了
* 一个请求同时包含 data | params 参数的设计本身不合理
* 不对这种情况进行兼容
* 2. 控制缓存内容的大小localStorage 只有5M
* 3. 控制缓存时间
* session(存在内存中)
* expireTime 存在localStoreage 中
* 4. 成功的、且响应内容为json的请求进行缓存
*/
/**
* 配置数据
* type: 'ram' | 'sessionStorage' | 'localStorage'
* cacheTime: ''
*/
/**
* 缓存数据结构
* cache: {
* url: 'url', // 缓存 url
* data: data, // 数据
* expire: '' // 缓存时间
* }
*/
/**
* 请求参数可以为如下类型
* - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
* - Browser only: FormData, File, Blob
* 只缓存参数类型为: string、plain object、URLSearchParams 或者无参数的 请求
*/
const CACHE_KEY_PREFIX = '__FES_REQUEST_CACHE:';
const CACHE_TYPE = {
ram: 'ram',
session: 'sessionStorage',
local: 'localStorage'
};
const CACHE_DATA_MAP = new Map();
function genInnerKey(key, cacheType = 'ram') {
if (cacheType !== CACHE_TYPE.ram) {
return `${CACHE_KEY_PREFIX}${key}`;
}
return key;
}
function canCache(data) {
return !data || isObject(data) || isString(data) || Array.isArray(data) || isURLSearchParams(data);
}
function setCacheData({
key,
cacheType = 'ram',
data,
cacheTime = 1000 * 60 * 3
}) {
const _key = genInnerKey(key, cacheType);
const currentCacheData = {
cacheType,
data,
cacheTime,
expire: Date.now() + cacheTime
};
if (cacheType !== CACHE_TYPE.ram) {
const cacheInstance = window[CACHE_TYPE[cacheType]];
try {
cacheInstance.setItem(_key, JSON.stringify(currentCacheData));
} catch (e) {
// setItem 出现异常,清理缓存
for (const item in cacheInstance) {
if (item.startsWith(CACHE_KEY_PREFIX) && Object.prototype.hasOwnProperty.call(cacheInstance, item)) {
cacheInstance.removeItem(item);
}
}
}
} else {
CACHE_DATA_MAP.set(_key, currentCacheData);
}
}
function isExpire({ expire, cacheTime }) {
if (!cacheTime || expire >= Date.now()) {
return false;
}
return true;
}
function getCacheData({ key, cacheType = 'ram' }) {
const _key = genInnerKey(key, cacheType);
if (cacheType !== CACHE_TYPE.ram) {
const cacheInstance = window[CACHE_TYPE[cacheType]];
const text = cacheInstance.getItem(_key) || null;
try {
const currentCacheData = JSON.parse(text);
if (currentCacheData && !isExpire(currentCacheData)) {
return currentCacheData.data;
}
cacheInstance.removeItem(_key);
return null;
} catch (e) {
cacheInstance.removeItem(_key);
return null;
}
} else {
const currentCacheData = CACHE_DATA_MAP.get(_key);
if (currentCacheData && !isExpire(currentCacheData)) {
return currentCacheData.data;
}
CACHE_DATA_MAP.delete(_key);
return null;
}
}
// 存储缓存队列
const cacheStartFlag = new Map();
const cachingQueue = new Map();
/**
* 等上一次请求结果
* 1. 如果上一次请求成功,直接使用上一次的请求结果
* 2. 如果上一次请求失败,重启本次请求
*/
function handleCachingStart(ctx, config) {
const _key = genInnerKey(ctx.key, config.cache.cacheType);
const caching = cacheStartFlag.get(_key);
if (caching) {
return new Promise((resolve) => {
const queue = cachingQueue.get(_key) || [];
cachingQueue.set(_key, queue.concat(resolve));
});
}
cacheStartFlag.set(_key, true);
}
// 有请求成功的
function handleCachingQueueSuccess(ctx, config) {
// 移除首次缓存 flag
const _key = genInnerKey(ctx.key, config.cache.cacheType);
const queue = cachingQueue.get(_key);
if (queue && queue.length > 0) {
queue.forEach((resolve) => {
resolve({
response: ctx.response
});
});
}
cachingQueue.delete(_key);
cacheStartFlag.delete(_key);
}
// 处理请求失败
function handleCachingQueueError(ctx, config) {
const _key = genInnerKey(ctx.key, config.cache.cacheType);
const queue = cachingQueue.get(_key);
if (queue && queue.length > 0) {
const firstResolve = queue.shift();
firstResolve();
cachingQueue.set(_key, queue);
} else {
cachingQueue.delete(_key);
cacheStartFlag.delete(_key);
}
}
export default async (ctx, next) => {
const { config } = ctx;
if (config.cache) {
const cacheData = getCacheData({ key: ctx.key, cacheType: config.cache.cacheType });
if (cacheData) {
ctx.response = {
data: cacheData
};
return;
}
const result = await handleCachingStart(ctx, config);
if (result) {
Object.keys(result).forEach((key) => {
ctx[key] = result[key];
});
return;
}
}
await next();
if (config.cache) {
const requestdata = checkHttpRequestHasBody(config.method) ? config.data : config.params;
if (!ctx.error && ctx.response && canCache(requestdata) && canCache(ctx.response.data)) {
handleCachingQueueSuccess(ctx, config);
setCacheData({
key: ctx.key,
data: ctx.response.data,
...config.cache
});
} else {
handleCachingQueueError(ctx, config);
}
}
};