From 97bfb30f30f448f0c63094ab1966994655bdbd63 Mon Sep 17 00:00:00 2001 From: qlin Date: Thu, 16 Jun 2022 11:04:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=20request=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 优化 request 插件 * fix: errorHandler 的顺序控制问题' --- docs/reference/plugin/plugins/request.md | 171 +++++++----------- packages/fes-plugin-request/package.json | 2 +- packages/fes-plugin-request/src/index.js | 32 ---- .../src/template/cacheControl.js | 22 +-- .../src/template/genRequestKey.js | 4 +- .../src/template/helpers.js | 24 +-- .../src/template/preventRepeatReq.js | 6 +- .../src/template/request.js | 81 ++------- .../src/template/resDataAdaptor.js | 17 -- .../src/template/resErrorProcess.js | 21 --- .../src/template/scheduler.js | 1 - .../src/template/setDataField.js | 11 -- packages/fes-plugin-request/types.d.ts | 8 +- packages/fes-template-h5/.fes.js | 3 - packages/fes-template-h5/src/app.js | 44 +++-- packages/fes-template-h5/src/pages/index.vue | 16 +- yarn.lock | 33 +++- 17 files changed, 167 insertions(+), 329 deletions(-) delete mode 100644 packages/fes-plugin-request/src/template/resDataAdaptor.js delete mode 100644 packages/fes-plugin-request/src/template/resErrorProcess.js delete mode 100644 packages/fes-plugin-request/src/template/setDataField.js diff --git a/docs/reference/plugin/plugins/request.md b/docs/reference/plugin/plugins/request.md index e9f6404a..1d688db1 100644 --- a/docs/reference/plugin/plugins/request.md +++ b/docs/reference/plugin/plugins/request.md @@ -1,6 +1,6 @@ # @fesjs/plugin-request -基于 axios 封装的 request,内置防止重复请求、请求节流、错误处理等功能。 +基于 axios 封装的 request,内置防止重复请求、请求缓存、错误处理等功能。 ## 启用方式 @@ -9,114 +9,86 @@ ```json { "dependencies": { - "@fesjs/fes": "^2.0.0", - "@fesjs/plugin-request": "^2.0.0" + "@fesjs/fes": "^3.0.0", + "@fesjs/plugin-request": "^3.0.0" } } ``` -## 配置 +## 运行时配置 -### 构建时配置 +入口文件的全局配置,具体请求的配置参数会覆盖全局配置,支持 [axios](https://axios-http.com/zh/docs/req_config) 所有的参数。 ```js -export default { +import { defineRuntimeConfig } from '@fesjs/fes'; + +export default defineRuntimeConfig({ request: { - dataField: 'result', - }, -}; -``` - -#### dataField - -- 类型: `string` -- 默认值: `''` -- 详情: - - `dataField` 对应接口中的数据字段。假设接口统一的规范是 `{ code: string, result: any}`,可配置 `dataField: 'result'`, 直接获取数据。如果个别接口不符合这个规范,可在第三个参数加上 `dataField: false`。 - -```js -// 构建时配置 dataField: 'result' - -import { request } from '@fesjs/fes'; - -// 假设相应体为: {code: '0', result: {say: 'hello'}} -const result = await request('/path/to/query/'); - -// {say: 'hello'} -console.log(result); - -// 假设相应体为: {code: '0', data: {say: 'hello'}},其中 result 字段换成了 data -const response1 = await request('/special/to/query/', null, { dataField: false }); - -// {code: '0', data: {say: 'hello'}} -console.log(response1); - -// 或者:假设相应体为: {code: '0', data: {say: 'hello'}},其中 result 字段换成了 data -const response2 = await request('/special/to/query/', null, { dataField: 'data' }); - -// {say: 'hello'} -console.log(response2); -``` - -### 运行时配置 - -在 `app.js` 中进行运行时配置。 - -```js -export const request = { - // 格式化 response.data (只有 response.data 类型为 object 才会调用) - responseDataAdaptor: (data) => { - data.code = data.code === '200' ? '0' : data.code; - return data; - }, - // 关闭 response data 校验(只判断 xhr status) - closeResDataCheck: false, - // 请求拦截器 - requestInterceptors: [], - // 响应拦截器 - responseInterceptors: [], - // 错误处理 - // 内部以 reponse.data.code === '0' 判断请求是否成功 - // 若使用其他字段判断,可以使用 responseDataAdaptor 对响应数据进行格式 - errorHandler: { - 11199(response) { - // 特殊 code 处理逻辑 + // API 前缀 + baseURL: '', + dataHandler(data, response) { + // 处理响应内容异常 + if (data.code !== '0') { + if (data.code === '10000') { + FMesseage.error('hello world'); + } + if (data.code === '20000') { + FMesseage.error('hello world'); + } + throw new Error(response); + } + // 响应数据格式化 + return data?.result ? data.result : data; }, - 404(error) {}, - default(error) { - // 异常统一处理 + // http 异常,和插件异常 + errorHandler(error) { + if (error.response) { + // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围 + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + } else if (error.request) { + // 请求已经成功发起,但没有收到响应 + // `error.request` 在浏览器中是 XMLHttpRequest 的实例, + // 而在node.js中是 http.ClientRequest 的实例 + console.log(error.request); + } else if (error.type) { + // 插件异常 + console.log(error.msg); + } else { + // 发送请求时出了点问题 + console.log('Error', error.message); + } + console.log(error.config); }, + // 请求拦截器 + requestInterceptors: [], + // 响应拦截器 + responseInterceptors: [], + // 支持其他 axios 配置 + ...otherConfigs, }, - // 其他 axios 配置 - ...otherConfigs, -}; +}); ``` -#### skipErrorHandler +## API -- 类型: `boolean | string | number | array` -- 默认值: `` -- 详情: +### request - 指定当前请求的某些错误状态不走 `errorHandler`,单独进行处理。如果设置为 `true`,当前请求的错误处理都不走 `errorHandler`。 +- **类型**:函数 -- 示列: +- **详情**:请求后端接口 +- **参数**: -```js -import { request } from '@fesjs/fes'; + - url: 后端接口 url + - data: 参数 + - options: 配置支持 [axios](https://axios-http.com/zh/docs/req_config) 所有的参数,和插件扩展参数。 -request('/api/login', null, { - skipErrorHandler: '110', -}) - .then((res) => { - // do something - }) - .catch((err) => { - // 这里处理 code 为 110 的异常 - // 此时 errorHandler[110] 函数不会生效,也不会执行 errorHandler.default - }); -``` +- **返回值**: Promise + +### useRequest + +request 的封装,返回响应式 `loading`、`error`、 `data` ## 使用 @@ -212,20 +184,3 @@ export default { }, }; ``` - -## API - -### request - -- **类型**:函数 - -- **详情**:请求后端接口 -- **参数**: - - url: 后端接口 url - - data: 参数 - - options:  配置( 支持 axios 所有配置) -- **返回值**: Promise - -### useRequest - -request 的封装,返回响应式 `loading`、`error`、 `data` diff --git a/packages/fes-plugin-request/package.json b/packages/fes-plugin-request/package.json index b08c2978..e384a91b 100644 --- a/packages/fes-plugin-request/package.json +++ b/packages/fes-plugin-request/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@fesjs/utils": "^3.0.0-beta.0", - "axios": "0.21.1" + "axios": "^1.0.0-alpha.1" }, "typings": "./types.d.ts" } diff --git a/packages/fes-plugin-request/src/index.js b/packages/fes-plugin-request/src/index.js index dd08120f..0e4fcef5 100644 --- a/packages/fes-plugin-request/src/index.js +++ b/packages/fes-plugin-request/src/index.js @@ -1,41 +1,11 @@ -import { readFileSync } from 'fs'; import { join } from 'path'; import { name } from '../package.json'; export default (api) => { api.addRuntimePluginKey(() => 'request'); - // 配置 - api.describe({ - key: 'request', - config: { - schema(joi) { - return joi.object({ - dataField: joi - .string() - .pattern(/^[a-zA-Z]*$/) - .allow(''), - base: joi.string().allow(''), - }); - }, - default: { - base: '', - dataField: '', - }, - }, - }); const namespace = 'plugin-request'; const absoluteFilePath = `${namespace}/request.js`; - const requestTemplate = readFileSync(join(__dirname, 'template', 'request.js'), 'utf-8'); - api.onGenerateFiles(() => { - // 文件写出 - const { dataField = '' } = api.config.request; - - api.writeTmpFile({ - path: absoluteFilePath, - content: requestTemplate.replace('REPLACE_DATA_FIELD', JSON.stringify(dataField)).replace('AXIOS_PATH', 'axios'), - }); - }); let generatedOnce = false; api.onGenerateFiles(() => { @@ -44,7 +14,6 @@ export default (api) => { api.copyTmpFiles({ namespace, path: join(__dirname, 'template'), - ignore: ['request.js'], }); }); @@ -58,6 +27,5 @@ export default (api) => { api.addConfigType(() => ({ source: name, runtime: ['RequestRuntimeConfig'], - build: ['RequestBuildConfig'], })); }; diff --git a/packages/fes-plugin-request/src/template/cacheControl.js b/packages/fes-plugin-request/src/template/cacheControl.js index 4de45132..fafb4439 100644 --- a/packages/fes-plugin-request/src/template/cacheControl.js +++ b/packages/fes-plugin-request/src/template/cacheControl.js @@ -1,6 +1,4 @@ -import { - isObject, isString, isURLSearchParams, checkHttpRequestHasBody -} from './helpers'; +import { isObject, isString, isURLSearchParams, checkHttpRequestHasBody } from './helpers'; /** * 缓存实现的功能 * 1. 唯一定位一个请求(url, data | params, method) @@ -20,7 +18,6 @@ import { * cacheTime: '' */ - /** * 缓存数据结构 * cache: { @@ -41,7 +38,7 @@ const CACHE_KEY_PREFIX = '__FES_REQUEST_CACHE:'; const CACHE_TYPE = { ram: 'ram', session: 'sessionStorage', - local: 'localStorage' + local: 'localStorage', }; const CACHE_DATA_MAP = new Map(); @@ -57,19 +54,14 @@ function canCache(data) { return !data || isObject(data) || isString(data) || Array.isArray(data) || isURLSearchParams(data); } -function setCacheData({ - key, - cacheType = 'ram', - data, - cacheTime = 1000 * 60 * 3 -}) { +function setCacheData({ key, cacheType = 'ram', data, cacheTime = 1000 * 60 * 3 }) { const _key = genInnerKey(key, cacheType); const currentCacheData = { cacheType, data, cacheTime, - expire: Date.now() + cacheTime + expire: Date.now() + cacheTime, }; if (cacheType !== CACHE_TYPE.ram) { const cacheInstance = window[CACHE_TYPE[cacheType]]; @@ -150,7 +142,7 @@ function handleCachingQueueSuccess(ctx, config) { if (queue && queue.length > 0) { queue.forEach((resolve) => { resolve({ - response: ctx.response + response: ctx.response, }); }); } @@ -178,7 +170,7 @@ export default async (ctx, next) => { const cacheData = getCacheData({ key: ctx.key, cacheType: config.cache.cacheType }); if (cacheData) { ctx.response = { - data: cacheData + data: cacheData, }; return; } @@ -200,7 +192,7 @@ export default async (ctx, next) => { setCacheData({ key: ctx.key, data: ctx.response.data, - ...config.cache + ...config.cache, }); } else { handleCachingQueueError(ctx, config); diff --git a/packages/fes-plugin-request/src/template/genRequestKey.js b/packages/fes-plugin-request/src/template/genRequestKey.js index 12c843a7..27bbbd95 100644 --- a/packages/fes-plugin-request/src/template/genRequestKey.js +++ b/packages/fes-plugin-request/src/template/genRequestKey.js @@ -14,9 +14,7 @@ const getQueryString = (data) => { }; export default async function genRequestKey(ctx, next) { - const { - url, data, params, method - } = ctx.config; + const { url, data, params, method } = ctx.config; ctx.key = `${url}${getQueryString(data)}${getQueryString(params)}${method}`; diff --git a/packages/fes-plugin-request/src/template/helpers.js b/packages/fes-plugin-request/src/template/helpers.js index 05b52320..97cdef0f 100644 --- a/packages/fes-plugin-request/src/template/helpers.js +++ b/packages/fes-plugin-request/src/template/helpers.js @@ -14,7 +14,7 @@ export function typeOf(obj) { '[object Undefined]': 'undefined', '[object Null]': 'null', '[object Object]': 'object', - '[object URLSearchParams]': 'URLSearchParams' + '[object URLSearchParams]': 'URLSearchParams', }; return map[Object.prototype.toString.call(obj)]; } @@ -43,36 +43,30 @@ export function isURLSearchParams(obj) { return typeOf(obj) === 'URLSearchParams'; } -// eslint-disable-next-line -export const isUndefined = val => val === undefined; - -export const isDefined = val => val != null; - - export function checkHttpRequestHasBody(method) { method = method.toUpperCase(); const HTTP_METHOD = { GET: { - request_body: false + request_body: false, }, POST: { - request_body: true + request_body: true, }, PUT: { - request_body: true + request_body: true, }, DELETE: { - request_body: true + request_body: true, }, HEAD: { - request_body: false + request_body: false, }, OPTIONS: { - request_body: false + request_body: false, }, PATCH: { - request_body: true - } + request_body: true, + }, }; return HTTP_METHOD[method].request_body; } diff --git a/packages/fes-plugin-request/src/template/preventRepeatReq.js b/packages/fes-plugin-request/src/template/preventRepeatReq.js index 562ff7ac..0b1505af 100644 --- a/packages/fes-plugin-request/src/template/preventRepeatReq.js +++ b/packages/fes-plugin-request/src/template/preventRepeatReq.js @@ -20,11 +20,11 @@ function handleRepeatRequest(ctx) { queue.forEach((resolve) => { if (ctx.error) { resolve({ - error: ctx.error + error: ctx.error, }); } else { resolve({ - response: ctx.response + response: ctx.response, }); } }); @@ -47,7 +47,7 @@ export default async (ctx, next) => { ctx.error = { type: 'REPEAT', msg: '重复请求', - config: ctx.config + config: ctx.config, }; return; } diff --git a/packages/fes-plugin-request/src/template/request.js b/packages/fes-plugin-request/src/template/request.js index fa32a085..a0a69193 100644 --- a/packages/fes-plugin-request/src/template/request.js +++ b/packages/fes-plugin-request/src/template/request.js @@ -1,16 +1,13 @@ -import axios from 'AXIOS_PATH'; +import axios from 'axios'; import { ApplyPluginsType, plugin } from '@fesjs/fes'; import { ref } from 'vue'; import scheduler from './scheduler'; import { checkHttpRequestHasBody, isFunction } from './helpers'; -import setDataField from './setDataField'; import paramsProcess from './paramsProcess'; import genRequestKey from './genRequestKey'; import preventRepeatReq from './preventRepeatReq'; import cacheControl from './cacheControl'; -import resDataAdaptor from './resDataAdaptor'; -import resErrorProcess from './resErrorProcess'; function addInterceptors(instance, interceptors, type = 'request') { interceptors.forEach((fn) => { @@ -41,10 +38,10 @@ async function axiosMiddleware(context, next) { function getRequestInstance() { const { - responseDataAdaptor, + dataHandler, + errorHandler, requestInterceptors = [], responseInterceptors = [], - errorHandler, ...otherConfigs } = plugin.applyPlugins({ key: 'request', @@ -65,23 +62,14 @@ function getRequestInstance() { addResponseInterceptors(instance, responseInterceptors); // 洋葱模型内部应该这是对数据的处理,避免有副作用调用 - scheduler - .use(paramsProcess) - .use(genRequestKey) - .use(cacheControl) - .use(preventRepeatReq) - .use(axiosMiddleware) - .use(resDataAdaptor) - .use(resErrorProcess) - .use(setDataField); + scheduler.use(paramsProcess).use(genRequestKey).use(cacheControl).use(preventRepeatReq).use(axiosMiddleware); return { context: { + errorHandler, + dataHandler: dataHandler || ((data) => data), instance, defaultConfig, - dataField: REPLACE_DATA_FIELD, // eslint-disable-line - responseDataAdaptor, - errorHandler, }, request: scheduler.compose(), }; @@ -110,52 +98,12 @@ function createContext(userConfig) { }; } -function getResponseCode(response) { - if (response) { - if (response._rawData) return response._rawData.code; - if (response.data) return response.data.code; - } - return null; -} - -function skipErrorHandlerToObj(skipErrorHandler = []) { - if (!Array.isArray(skipErrorHandler)) { - skipErrorHandler = [skipErrorHandler]; - } - - return skipErrorHandler.reduce((acc, cur) => { - acc[cur] = true; - return acc; - }, {}); -} - -function getErrorKey(error, response) { - const resCode = getResponseCode(response); - - if (resCode) return resCode; - if (error.type) return error.type; - return error.response?.status; -} - -function isSkipErrorHandler(config, errorKey) { - // 跳过所有错误类型处理 - if (config.skipErrorHandler === true) return true; - - const skipObj = skipErrorHandlerToObj(config.skipErrorHandler); - - return skipObj[errorKey]; -} - -function handleRequestError({ errorHandler = {}, error, response, config }) { - const errorKey = getErrorKey(error, response); - - if (!isSkipErrorHandler(config, errorKey)) { - if (isFunction(errorHandler[errorKey])) { - errorHandler[errorKey](error, response); - } else if (isFunction(errorHandler.default)) { - errorHandler.default(error, response); - } - } +function getCustomerHandler(ctx, options = {}) { + const { dataHandler, errorHandler } = ctx; + return { + dataHandler: options.dataHandler || dataHandler, + errorHandler: options.errorHandler || errorHandler, + }; } export const request = (url, data, options = {}) => { @@ -169,12 +117,13 @@ export const request = (url, data, options = {}) => { } const userConfig = userConfigHandler(url, data, options); const context = createContext(userConfig); + const { dataHandler, errorHandler } = getCustomerHandler(context, options); return currentRequestInstance.request(context).then(async () => { if (!context.error) { - return context.config.useResponse ? context.response : context.response.data; + return dataHandler(context.response.data, context.response); } - await handleRequestError(context); + errorHandler && errorHandler(context.error); return Promise.reject(context.error); }); }; diff --git a/packages/fes-plugin-request/src/template/resDataAdaptor.js b/packages/fes-plugin-request/src/template/resDataAdaptor.js deleted file mode 100644 index 91d3a0f8..00000000 --- a/packages/fes-plugin-request/src/template/resDataAdaptor.js +++ /dev/null @@ -1,17 +0,0 @@ -import { isFunction, isObject, isString } from './helpers'; - -export default async ({ response, responseDataAdaptor }, next) => { - // 如果 data 是 blob 并且 content-type 是 application/json,自动进行数据处理 - if (response && response.data instanceof Blob && response.headers['content-type'].startsWith('application/json') && response.data.type === 'application/json') { - const rawData = response.data; - try { - response.data = JSON.parse(await response.data.text()); - } catch { - response.data = rawData; - } - } - if (isFunction(responseDataAdaptor) && response && (isObject(response.data) || isString(response.data))) { - response.data = responseDataAdaptor(response.data); - } - await next(); -}; diff --git a/packages/fes-plugin-request/src/template/resErrorProcess.js b/packages/fes-plugin-request/src/template/resErrorProcess.js deleted file mode 100644 index 68a9fcb6..00000000 --- a/packages/fes-plugin-request/src/template/resErrorProcess.js +++ /dev/null @@ -1,21 +0,0 @@ -import { isObject } from './helpers'; - -// 错误处理等副作用网上提 -export default async (ctx, next) => { - const { - response, - config - } = ctx; - if (!config.closeResDataCheck && response && isObject(response.data)) { - const code = response.data.code; - if (code !== '0') { - // 尽量保持内部 error 结构和 http 异常的 error 结构一致 - ctx.error = { - ...response, - response - }; - } - } - - await next(); -}; diff --git a/packages/fes-plugin-request/src/template/scheduler.js b/packages/fes-plugin-request/src/template/scheduler.js index 5e62120d..bcecd266 100644 --- a/packages/fes-plugin-request/src/template/scheduler.js +++ b/packages/fes-plugin-request/src/template/scheduler.js @@ -1,4 +1,3 @@ - class Scheduler { constructor() { this.middlewares = []; diff --git a/packages/fes-plugin-request/src/template/setDataField.js b/packages/fes-plugin-request/src/template/setDataField.js deleted file mode 100644 index 1cf598c9..00000000 --- a/packages/fes-plugin-request/src/template/setDataField.js +++ /dev/null @@ -1,11 +0,0 @@ -import { isObject } from './helpers'; - -// FEATURE: 后续支持 a.b.c -export default async (ctx, next) => { - const dataField = ctx.config.dataField ?? ctx.dataField; - if (!ctx.error && ctx.response && isObject(ctx.response.data) && dataField) { - ctx.response._rawData = ctx.response.data; - ctx.response.data = ctx.response.data[dataField]; - } - await next(); -}; diff --git a/packages/fes-plugin-request/types.d.ts b/packages/fes-plugin-request/types.d.ts index 2efbebec..02aefaf2 100644 --- a/packages/fes-plugin-request/types.d.ts +++ b/packages/fes-plugin-request/types.d.ts @@ -1,11 +1,5 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios'; -export interface RequestBuildConfig { - request: { - dataField: string - } -} - type RequestInterceptor = (value: AxiosRequestConfig) => AxiosRequestConfig | [(value: AxiosRequestConfig) => AxiosRequestConfig, (error: any) => any]; type ResponseInterceptor = (value: AxiosResponse) => AxiosResponse | [(value: AxiosResponse) => AxiosResponse, (error: any) => any]; @@ -17,7 +11,7 @@ export interface RequestRuntimeConfig { closeResDataCheck?: boolean; requestInterceptors?: RequestInterceptor[]; responseInterceptors?: ResponseInterceptor[]; - errorHandler: { + errorHandler?: { [key: string]: (error: { response: AxiosResponse } | AxiosResponse) => void; }; } & AxiosRequestConfig; diff --git a/packages/fes-template-h5/.fes.js b/packages/fes-template-h5/.fes.js index cd83bfa4..d3755d4b 100644 --- a/packages/fes-template-h5/.fes.js +++ b/packages/fes-template-h5/.fes.js @@ -10,9 +10,6 @@ export default defineBuildConfig({ } }, publicPath: '/', - request: { - dataField: 'result' - }, html: { title: '拉夫德鲁' }, diff --git a/packages/fes-template-h5/src/app.js b/packages/fes-template-h5/src/app.js index c0a707f4..cbb3e52d 100644 --- a/packages/fes-template-h5/src/app.js +++ b/packages/fes-template-h5/src/app.js @@ -2,18 +2,38 @@ import { defineRuntimeConfig } from '@fesjs/fes'; export default defineRuntimeConfig({ request: { - errorHandler: { - 111() { - console.log('root:111'); - }, - 500() { - console.log('500 error'); - }, - default(error) { - console.log(error); - const msg = error?.data?.msg || error?.msg; - console.log(msg); - }, + baseURL: '/ras-mas', + dataHandler(data) { + if (data?.code !== '0') { + if (data.code === '10000') { + console.log('code', data.code); + } + if (data?.code === '20000') { + console.log('code', data.code); + } + throw new Error(data); + } + return data.result ? data.result : data; + }, + errorHandler(error) { + if (error.response) { + // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围 + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + } else if (error.request) { + // 请求已经成功发起,但没有收到响应 + // `error.request` 在浏览器中是 XMLHttpRequest 的实例, + // 而在node.js中是 http.ClientRequest 的实例 + console.log(error.request); + } else if (error.type) { + // 插件异常 + console.log(error.msg); + } else { + // 发送请求时出了点问题 + console.log('Error', error.message); + } + console.log(error.config); }, }, patchRoutes: () => { diff --git a/packages/fes-template-h5/src/pages/index.vue b/packages/fes-template-h5/src/pages/index.vue index b41b9d71..a3e140ad 100644 --- a/packages/fes-template-h5/src/pages/index.vue +++ b/packages/fes-template-h5/src/pages/index.vue @@ -7,7 +7,7 @@