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); } } };