Open nitroge opened 1 year ago
由于需要基于 axios 封装自己业务请求库,主要解决问题就是把通用操作封装,减少重复操作,同样响应错误码进行集中管理,这样可以更加侧重在业务上的开发.
axios
往往每新开发一个项目都需要去重写或复制老项目,同样当后端新增或修改业务 code 时,都需要 N 个项目做修改和维护。 解决这个问题就是在 axios 基于上封装一层,这层处理通用型的问题和把后端状态进行集中式的管理。
code
N
后端的变化之需要从 N端更改,改为一端更改。大大提升开发效率和维护成本。
封装自己的业务插件,做到如下两点:
假设插件的使用方式与 axios 完全不一样,对于用户来说需要熟悉成本,同样没办法做到平替(可以观察websocket-reconnect - npm第三方库,基于 websocket 进行封装,保留原生 websocket 相应的入参、事件。只是其基础上封装重连等功能)。
websocket
可扩展 毫无疑问也很重要:
保留原有行为很好实现,我们只需要把 axios 实例返回即可。
import axios, { AxiosRequestConfig } from 'axios'; import { ResultCodeEnum, ErrorCodeMap } from './code'; import { onRequestFulfilled, onRejected } from './requestInterceptor'; import { onResponseFulfilled, onResponseRejected } from './responseInterceptor'; // 默认参数 const defaultOptions: AxiosRequestConfig = { baseURL: '', timeout: 15000, }; // 扩展参数 export interface Options extends AxiosRequestConfig { getToken?: () => string; loginOut?: () => void; notify: (msg: string) => void; } // 导出请求状态码 export { ResultCodeEnum, ErrorCodeMap }; // 导出请求方法 export default function request(options?: Options) { // 合并选项 let optionsConfig: Options; if (options) { optionsConfig = { ...options, ...defaultOptions, notify: options?.notify && typeof options.notify === 'function' ? options.notify : (message) => { console.error(message); }, }; } else { optionsConfig = { ...defaultOptions, notify: (message) => { console.error(message); }, }; } // 创建实例 const instance = axios.create(optionsConfig); // 添加请求拦截器 instance.interceptors.request.use((config) => { return onRequestFulfilled(config, optionsConfig); }, onRejected); // 添加响应拦截器 instance.interceptors.response.use( (response) => { return onResponseFulfilled(response, optionsConfig); }, (error) => { return onResponseRejected(error, optionsConfig); } ); return instance; }
// requestInterceptor.ts import { AxiosError, AxiosRequestConfig } from 'axios'; import { Options } from './request'; export function onRequestFulfilled( config: AxiosRequestConfig, optionsConfig: Options ) { if (config.headers) { if (optionsConfig && optionsConfig.getToken && optionsConfig.getToken()) { config.headers.Authorization = optionsConfig.getToken(); } } return config; } export function onRejected(error: AxiosError) { return Promise.reject(error); }
// responseInterceptor.ts import { AxiosError, AxiosResponse } from 'axios'; import { ResultCodeEnum } from './code'; import { Options } from './request'; export function onResponseFulfilled( response: AxiosResponse, optionsConfig: Options ) { const { data } = response; if (data.code !== ResultCodeEnum.SUCCESS) { optionsConfig.notify(data.message); if ( data.code === ResultCodeEnum.TOKEN_EXPIRE || data.code === ResultCodeEnum.TOKEN_FAIL ) { if (optionsConfig && optionsConfig.loginOut) { optionsConfig.loginOut(); } } return Promise.reject(new Error(data.message || 'Error')); } return data; } export function onResponseRejected(error: AxiosError, optionsConfig: Options) { // 处理 500 状态码 if (error.response) { const { status } = error.response; if (status === 500) { optionsConfig.notify('服务开小差了!!!'); } else if (status === 404) { optionsConfig.notify('资源找不到!!!'); } else if (status === 401) { optionsConfig.notify('无权限访问!!!'); } else if (status === 403) { optionsConfig.notify('拒绝访问!!!'); } } else { // 请求超时 if (error.code === 'ECONNABORTED') { optionsConfig.notify('请求超时'); } } return Promise.reject(error); }
// code.ts enum ResultCodeEnum { SUCCESS = 'SUCCESS', // 操作成功 BIZ_ERROR = 'BIZ_ERROR', // 业务处理异常 INTERFACE_SYSTEM_ERROR = 'INTERFACE_SYSTEM_ERROR', // 外部接口调用异常 CONNECT_TIME_OUT = 'CONNECT_TIME_OUT', // 系统超时 NULL_ARGUMENT = 'NULL_ARGUMENT', // 参数为空 ILLEGAL_ARGUMENT = 'ILLEGAL_ARGUMENT', // 参数不合法 ILLEGAL_REQUEST = 'ILLEGAL_REQUEST', // 非法请求 METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED', // 请求方法不允许 ILLEGAL_CONFIGURATION = 'ILLEGAL_CONFIGURATION', // 配置不合法 ILLEGAL_STATE = 'ILLEGAL_STATE', // 状态不合法 ENUM_CODE_ERROR = 'ENUM_CODE_ERROR', // 错误的枚举编码 LOGIC_ERROR = 'LOGIC_ERROR', // 逻辑错误 CONCURRENT_ERROR = 'CONCURRENT_ERROR', // 并发异常 ILLEGAL_OPERATION = 'ILLEGAL_OPERATION', // 非法操作 REPETITIVE_OPERATION = 'REPETITIVE_OPERATION', // 重复操作 NO_OPERATE_PERMISSION = 'NO_OPERATE_PERMISSION', // 无操作权限 RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', // 资源不存在 RESOURCE_ALREADY_EXIST = 'RESOURCE_ALREADY_EXIST', // 资源已存在 TYPE_UN_MATCH = 'TYPE_UN_MATCH', // 类型不匹配 FILE_NOT_EXIST = 'FILE_NOT_EXIST', // 文件不存在 LIMIT_BLOCK = 'LIMIT_BLOCK', // 请求限流阻断 TOKEN_FAIL = 'TOKEN_FAIL', // token校验失败 TOKEN_EXPIRE = 'TOKEN_EXPIRE', // token过期 REQUEST_EXCEPTION = 'REQUEST_EXCEPTION', // 请求异常 BLOCK_EXCEPTION = 'BLOCK_EXCEPTION', // 接口限流降级 SYSTEM_ERROR = 'SYSTEM_ERROR', // ❌系统异常 } const ErrorCodeMap = { [ResultCodeEnum.SUCCESS]: '操作成功', [ResultCodeEnum.BIZ_ERROR]: '业务处理异常', [ResultCodeEnum.INTERFACE_SYSTEM_ERROR]: '外部接口调用异常', [ResultCodeEnum.CONNECT_TIME_OUT]: '系统超时', [ResultCodeEnum.NULL_ARGUMENT]: '参数为空', [ResultCodeEnum.ILLEGAL_ARGUMENT]: '参数不合法', [ResultCodeEnum.ILLEGAL_REQUEST]: '非法请求', [ResultCodeEnum.METHOD_NOT_ALLOWED]: '请求方法不允许', [ResultCodeEnum.ILLEGAL_CONFIGURATION]: '配置不合法', [ResultCodeEnum.ILLEGAL_STATE]: '状态不合法', [ResultCodeEnum.ENUM_CODE_ERROR]: '错误的枚举编码', [ResultCodeEnum.LOGIC_ERROR]: '逻辑错误', [ResultCodeEnum.CONCURRENT_ERROR]: '并发异常', [ResultCodeEnum.ILLEGAL_OPERATION]: '非法操作', [ResultCodeEnum.REPETITIVE_OPERATION]: '重复操作', [ResultCodeEnum.NO_OPERATE_PERMISSION]: '无操作权限', [ResultCodeEnum.RESOURCE_NOT_FOUND]: '资源不存在', [ResultCodeEnum.RESOURCE_ALREADY_EXIST]: '资源已存在', [ResultCodeEnum.TYPE_UN_MATCH]: '类型不匹配', [ResultCodeEnum.FILE_NOT_EXIST]: '文件不存在', [ResultCodeEnum.LIMIT_BLOCK]: '请求限流阻断', [ResultCodeEnum.TOKEN_FAIL]: 'token校验失败', [ResultCodeEnum.TOKEN_EXPIRE]: 'token过期', [ResultCodeEnum.REQUEST_EXCEPTION]: '请求异常', [ResultCodeEnum.BLOCK_EXCEPTION]: '接口限流降级', [ResultCodeEnum.SYSTEM_ERROR]: '❌系统异常', }; export { ResultCodeEnum, ErrorCodeMap };
上面封装只做几件事:
上面封装基于大前提就是,各个业务系统后端标准是一样。
// 创建实力 const instance = request({ baseURL: 'http://localhost:3000', getToken() { return '123123123'; }, notify(msg) { console.log(msg); }, loginOut() { console.log('loginOut'); }, }); // 定义拦截器 instance.interceptors.response.use( (res) => { return res.data; }, (err) => { return Promise.reject(err); } ); // 发送请求 instance.get('/api/test').then((res) => { console.log(res); });
在 axios 除了对请求数据相关处理之外,另一个比较重要的点就是拦截器。 我们能否使用好,取决于这对些核心概念的理解。
axios 拦截器也是采用经典的洋葱模型,如下图所示
洋葱模型
为什么要采用洋葱模型?洋葱模型有什么好处。 这里我把自己理解说下(仅是个人理解)
可以先看看核心源码部分:
// filter out skipped interceptors var requestInterceptorChain = []; var synchronousRequestInterceptors = true; this.interceptors.request.forEach(function unshiftRequestInterceptors( interceptor ) { if ( typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false ) { return; } synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); var responseInterceptorChain = []; this.interceptors.response.forEach(function pushResponseInterceptors( interceptor ) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); var promise; if (!synchronousRequestInterceptors) { var chain = [dispatchRequest, undefined]; Array.prototype.unshift.apply(chain, requestInterceptorChain); chain = chain.concat(responseInterceptorChain); promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; } var newConfig = config; while (requestInterceptorChain.length) { var onFulfilled = requestInterceptorChain.shift(); var onRejected = requestInterceptorChain.shift(); try { newConfig = onFulfilled(newConfig); } catch (error) { onRejected(error); break; } } try { promise = dispatchRequest(newConfig); } catch (error) { return Promise.reject(error); } while (responseInterceptorChain.length) { promise = promise.then( responseInterceptorChain.shift(), responseInterceptorChain.shift() ); }
上面的代码可以转化为 4 步:
Chain
下面使用简单案例:
const instance = request({ baseURL: 'http://localhost:3000', //... }); instance.interceptors.request.use( function outRequestFulfilled(config) { return config; }, function outRejected(err) { return Promise.reject(err); } ); instance.interceptors.response.use( function outResponseFulfilled(res) { return res.data; }, function outResponseRejected(err) { return Promise.reject(err); } ); instance.get('/api/test').then((res) => { console.log(res); });
上面代码构建的拦截器链如下图:
这样结合前面的洋葱图,是不是跟上面箭头指向顺序完全吻合。
看下如下代码:
promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
这里对理解和对错误拦截处理很重要。
先停下来看这个简单的代码执行应该是什么:
const promsie = new Promise((resolve, reject) => { resolve(); }) .then( function resolve1() { throw new Error('执行错误'); }, function reject1() { console.log('1. reject'); // 1. reject } ) .then( function resolve2() { console.log('2. resolve'); // 2. resolve }, function reject3() { console.log('3. reject'); // 3. reject } ) .catch(function reject4() { console.log('4. reject'); // 4. reject });
上面这个代码执行后是这样的结果:
// 3. reject
为什么会是这样,再思考一下:
promise
padding
resolve
reject
resolve1
throw new Error("执行错误")
reject1
throw
Promise.reject(Error('执行错误')
reject3
catch
弄懂这里之后,再回过头看:
把上面案例拿出来,当下面代码执行时,正常打印输出:
const instance = request({ baseURL: 'http://localhost:3000', getToken() { return '123123123'; }, loginOut() { console.log('loginOut'); }, notify(msg) { console.log(msg); }, }); instance.interceptors.request.use( function outRequestFulfilled(config) { throw new Error('主动抛出错误'); return config; }, function outRequestRejected(err) { console.error('outRequestRejected'); return Promise.reject(err); } ); instance.interceptors.response.use( function outResponseFulfilled(res) { return res.data; }, function outResponseRejected(err) { console.error('outResponseRejected'); return Promise.reject(err); } ); instance.get('/api/test').then((res) => { console.log(res); });
如果理解前面简单 promise 案例,对着上面 chain 链表应该就能知道执行顺序了。
chain
下面代码运行后的结果:
具体可以实际写一个 DEMO 实操一遍。
请求拦截器
先执行
响应拦截器
后执行
在 chain 中有一个 dispatchRequest 它的用途是啥?
dispatchRequest
由于需要基于
axios
封装自己业务请求库,主要解决问题就是把通用操作封装,减少重复操作,同样响应错误码进行集中管理,这样可以更加侧重在业务上的开发.往往每新开发一个项目都需要去重写或复制老项目,同样当后端新增或修改业务
code
时,都需要N
个项目做修改和维护。 解决这个问题就是在 axios 基于上封装一层,这层处理通用型的问题和把后端状态进行集中式的管理。后端的变化之需要从
N
端更改,改为一端更改。大大提升开发效率和维护成本。封装自己的业务插件,做到如下两点:
axios
一样)保留原有行为
假设插件的使用方式与
axios
完全不一样,对于用户来说需要熟悉成本,同样没办法做到平替(可以观察websocket-reconnect - npm第三方库,基于 websocket 进行封装,保留原生websocket
相应的入参、事件。只是其基础上封装重连等功能)。可扩展
可扩展 毫无疑问也很重要:
接下来简单封装一下
保留原有行为很好实现,我们只需要把
axios
实例返回即可。上面封装只做几件事:
上面封装基于大前提就是,各个业务系统后端标准是一样。
通过简单案例使用
扩展
在
axios
除了对请求数据相关处理之外,另一个比较重要的点就是拦截器。 我们能否使用好,取决于这对些核心概念的理解。拦截器原理
axios
拦截器也是采用经典的洋葱模型
,如下图所示为什么要采用洋葱模型?洋葱模型有什么好处。 这里我把自己理解说下(仅是个人理解)
拦截器执行顺序
可以先看看核心源码部分:
上面的代码可以转化为 4 步:
Chain
下面使用简单案例:
上面代码构建的拦截器链如下图:
这样结合前面的洋葱图,是不是跟上面箭头指向顺序完全吻合。
看下如下代码:
这里对理解和对错误拦截处理很重要。
先停下来看这个简单的代码执行应该是什么:
上面这个代码执行后是这样的结果:
为什么会是这样,再思考一下:
promise
状态流转是不可逆的,也就是只能从padding
->resolve
|reject
.resolve1
时执行throw new Error("执行错误")
,此时上一次promise
已经状态从padding
->resolve
,这就是为什么不会进入到reject1
的原因。throw
一个错误,虽然没显示返回新的promise
时,但是自动包装成 ·Promise.reject(Error('执行错误')
,也就是会执行到reject3
原因。catch
? 因为错误并没继续抛出(也就是传递)弄懂这里之后,再回过头看:
把上面案例拿出来,当下面代码执行时,正常打印输出:
如果理解前面简单 promise 案例,对着上面
chain
链表应该就能知道执行顺序了。下面代码运行后的结果:
具体可以实际写一个 DEMO 实操一遍。
总结
请求拦截器
后加入先执行
,响应拦截器
后加入后执行
。思考一下
在
chain
中有一个dispatchRequest
它的用途是啥?