From 8280e5f3a997a7fbc5703dd8cc03f4b2b6ceb00a Mon Sep 17 00:00:00 2001 From: bac-joker Date: Tue, 29 Dec 2020 17:44:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20request=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/template/cacheControl.js | 145 ++++++++++++++++++ .../src/template/paramsProcess.js | 17 +- .../src/template/request.js | 78 +++++++--- .../src/template/resDataAdaptor.js | 13 +- .../src/template/resErrorProcess.js | 34 ++-- .../src/template/scheduler.js | 33 ++++ .../src/template/setDataField.js | 14 +- 7 files changed, 270 insertions(+), 64 deletions(-) create mode 100644 packages/fes-plugin-request/src/template/cacheControl.js create mode 100644 packages/fes-plugin-request/src/template/scheduler.js diff --git a/packages/fes-plugin-request/src/template/cacheControl.js b/packages/fes-plugin-request/src/template/cacheControl.js new file mode 100644 index 00000000..e7f60969 --- /dev/null +++ b/packages/fes-plugin-request/src/template/cacheControl.js @@ -0,0 +1,145 @@ +import { + genRequestKey, 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 = new Map(); + +function genInnerKey(key, cacheType) { + if (cacheType !== CACHE_TYPE.ram) { + return `${CACHE_KEY_PREFIX}${key}`; + } + return key; +} + +function canCache(requestData) { + return isObject(requestData) || isString(requestData) || isURLSearchParams(requestData); +} + +function setCacheData({ + key, + cacheType, + 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.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.get(_key); + if (currentCacheData && !isExpire(currentCacheData)) { + return currentCacheData.data; + } + CACHE_DATA.delete(_key); + return null; + } +} + +export default (ctx, next) => { + const { config } = ctx; + let requestKey; + if (config.cache) { + const data = checkHttpRequestHasBody(config.method) ? config.data : config.params; + requestKey = genRequestKey(config.url, data, config.method); + if (canCache(data)) { + ctx.response = { + data: getCacheData({ key: requestKey, cacheType: config.cache.cacheType }) + }; + return; + } + } + next(); + + if (config.cache) { + setCacheData({ + key: requestKey, + data: ctx.response.data, + ...config.cache + }); + } +}; diff --git a/packages/fes-plugin-request/src/template/paramsProcess.js b/packages/fes-plugin-request/src/template/paramsProcess.js index 47ce5a85..08b34cd8 100644 --- a/packages/fes-plugin-request/src/template/paramsProcess.js +++ b/packages/fes-plugin-request/src/template/paramsProcess.js @@ -1,12 +1,11 @@ import { checkHttpRequestHasBody, trimObj } from 'helpers'; -export default (instance) => { - instance.interceptors.request.use((config) => { - if (checkHttpRequestHasBody(config.method)) { - config.data = trimObj(config.data); - } else { - config.params = trimObj(config.params); - } - return config; - }); +export default (ctx, next) => { + const config = ctx.config; + if (checkHttpRequestHasBody(config.method)) { + config.data = trimObj(config.data); + } else { + config.params = trimObj(config.params); + } + next(); }; diff --git a/packages/fes-plugin-request/src/template/request.js b/packages/fes-plugin-request/src/template/request.js index 7ab218cf..5229c64d 100644 --- a/packages/fes-plugin-request/src/template/request.js +++ b/packages/fes-plugin-request/src/template/request.js @@ -1,5 +1,6 @@ import axios from 'axios'; import { ApplyPluginsType, plugin } from '@webank/fes'; +import scheduler from 'scheduler'; import { checkHttpRequestHasBody, isFunction @@ -28,6 +29,16 @@ function addResponseInterceptors(instance, interceptors) { addInterceptors(instance, interceptors, 'response'); } +function axiosMiddleware(context, next) { + context.instance.request(context.config).then((response) => { + context.response = response; + }).catch((error) => { + context.error = error; + }).finally(() => { + next(); + }); +} + function getRequestInstance() { const { responseDataAdaptor, @@ -42,33 +53,36 @@ function getRequestInstance() { initialValue: {} }); - const instance = axios.create(Object.assign({ + const defaultConfig = Object.assign({ timeout: 10000, withCredentials: true - }, otherConfigs)); + }, otherConfigs); + const instance = axios.create(defaultConfig); - // eslint-disable-next-line - const dataField = REPLACE_DATA_FIELD; - addRequestInterceptors(requestInterceptors); - addResponseInterceptors(responseInterceptors); + addRequestInterceptors(instance, requestInterceptors); + addResponseInterceptors(instance, responseInterceptors); - - paramsProcess(instance); - resDataAdaptor(instance, { responseDataAdaptor }); - resErrorProcess(instance, { errorConfig, errorHandler }); - setDataField(instance, dataField); + scheduler.use(paramsProcess); + scheduler.use(axiosMiddleware); + scheduler.use(resDataAdaptor); + scheduler.use(resErrorProcess); + scheduler.use(setDataField); return { - instance + context: { + instance, + defaultConfig, + dataField: REPLACE_DATA_FIELD, // eslint-disable-line + responseDataAdaptor, + errorConfig, + errorHandler + }, + request: scheduler.compose() }; } -let currentRequestInstance = null; -export const request = (url, data, options = {}) => { - if (!currentRequestInstance) { - const { instance } = getRequestInstance(); - currentRequestInstance = instance; - } + +function userConfigHandler(url, data, options = {}) { options.url = url; options.method = (options.method || 'post').toUpperCase(); if (checkHttpRequestHasBody(options.method)) { @@ -76,5 +90,31 @@ export const request = (url, data, options = {}) => { } else { options.params = data; } - return currentRequestInstance.request(options); +} + +let currentRequestInstance = null; + +function createContext(userConfig) { + return { + ...currentRequestInstance.context, + config: { + ...currentRequestInstance.context.defaultConfig, + ...userConfig + } + }; +} + +export const request = (url, data, options = {}) => { + if (!currentRequestInstance) { + currentRequestInstance = getRequestInstance(); + } + const userConfig = userConfigHandler(url, data, options); + const context = createContext(userConfig); + + return currentRequestInstance.request(context).then((ctx) => { + if (!ctx.error) { + return ctx.config.useResonse ? ctx.response : ctx.response.data; + } + return Promise.reject(ctx.error); + }); }; diff --git a/packages/fes-plugin-request/src/template/resDataAdaptor.js b/packages/fes-plugin-request/src/template/resDataAdaptor.js index 896a85a4..004a495f 100644 --- a/packages/fes-plugin-request/src/template/resDataAdaptor.js +++ b/packages/fes-plugin-request/src/template/resDataAdaptor.js @@ -1,11 +1,8 @@ import { isFunction, isObject, isString } from './helpers'; -export default (instance, { responseDataAdaptor }) => { - instance.interceptors.response.use((response) => { - // 响应内容可能是个文件流 or 普通文本 - if (isFunction(responseDataAdaptor) && (isObject(response.data) || isString(response.data))) { - response.data = responseDataAdaptor(response.data); - } - return response; - }); +export default ({ response, responseDataAdaptor }, next) => { + if (isFunction(responseDataAdaptor) && (isObject(response.data) || isString(response.data))) { + response.data = responseDataAdaptor(response.data); + } + next(); }; diff --git a/packages/fes-plugin-request/src/template/resErrorProcess.js b/packages/fes-plugin-request/src/template/resErrorProcess.js index 53de4c8b..560d422f 100644 --- a/packages/fes-plugin-request/src/template/resErrorProcess.js +++ b/packages/fes-plugin-request/src/template/resErrorProcess.js @@ -8,30 +8,22 @@ function resErrorProcess(error, customerErrorHandler) { } } -export default (instance, { errorConfig, errorHandler }) => { + +export default ({ + error, + errorConfig, + errorHandler, + response +}, next) => { const _errorConfig = Object.assign({ - 401: { - showType: 9, - errorPage: '/login' - }, 403: '用户得到授权,但访问是禁止的' }, errorConfig); - instance.interceptors.response.use((response) => { - if (isObject(response.data) && response.data.code !== '0') { - resErrorProcess(_errorConfig[response.data.code] || response.data.msg || response.data.errorMessage || response.data.errorMsg || '服务异常', errorHandler); - return Promise.reject(response); - } + if (isObject(response.data) && response.data.code !== '0') { + resErrorProcess(_errorConfig[response.data.code] || response.data.msg || response.data.errorMessage || response.data.errorMsg || '服务异常', errorHandler); + } else if (error && error.response && _errorConfig[error.response.status]) { + resErrorProcess(_errorConfig[error.response.status], errorHandler); + } - return response; - }, (error) => { - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - if (_errorConfig[error.response.status]) { - resErrorProcess(_errorConfig[error.response.status], errorHandler); - } - } - return Promise.reject(error); - }); + next(); }; diff --git a/packages/fes-plugin-request/src/template/scheduler.js b/packages/fes-plugin-request/src/template/scheduler.js new file mode 100644 index 00000000..b00906d1 --- /dev/null +++ b/packages/fes-plugin-request/src/template/scheduler.js @@ -0,0 +1,33 @@ + +class Scheduler { + constructor() { + this.middlewares = []; + } + + use(fn) { + if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); + this.middlewares.push(fn); + return this; + } + + compose() { + return (context, next) => { + let index = -1; + const dispatch = (i) => { + if (i <= index) return Promise.reject(new Error('next() called multiple times')); + index = i; + let fn = this.middlewares[i]; + if (index === this.middlewares.length) fn = next; + if (!fn) return Promise.resolve(); + try { + return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); + } catch (e) { + return Promise.reject(e); + } + }; + return dispatch(0); + }; + } +} + +export default Scheduler(); diff --git a/packages/fes-plugin-request/src/template/setDataField.js b/packages/fes-plugin-request/src/template/setDataField.js index ec06af0a..07c71c20 100644 --- a/packages/fes-plugin-request/src/template/setDataField.js +++ b/packages/fes-plugin-request/src/template/setDataField.js @@ -1,10 +1,10 @@ import { isObject } from './helpers'; -export default (instance, { dataField }) => { - instance.interceptors.response.use((response) => { - if (isObject(response.data) && dataField) { - return response.data[dataField]; - } - return response; - }); +export default (ctx, next) => { + const { dataField, response } = ctx; + if (isObject(response.data) && dataField) { + ctx.response._rawData = response.data; + ctx.response.data = response.data[dataField]; + } + next(); };