共计 9622 个字符,预计需要花费 25 分钟才能阅读完成。
因为须要基于 axios
封装本人业务申请库,次要解决问题就是把通用操作封装,缩小反复操作,同样响应错误码进行集中管理,这样能够更加偏重在业务上的开发.
封装本人的业务插件,做到如下两点:
- 不减少用户应用老本(应用行为上同
axios
一样) - 可扩大
保留原有行为
假如插件的应用形式与 axios
齐全不一样,对于用户来说须要相熟老本,同样没方法做到平替(能够察看websocket-reconnect – npm 第三方库,基于 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}; |
下面封装只做几件事:
- 定义默认参数值,缩小用户传参
- 对参数进行校验,避免异常情况
- 扩大参数选项,保留原有 Axios 能力,扩大基于业务相干的选项
- 分模块治理拦截器
- 提供内部增加申请、响应拦截器能力
- 外部拦截器不扭转业务零碎原有的响应,从而让内部零碎拿到残缺后端响应后果
- 错误码对立治理
- 增加通用谬误拦挡、判断、提醒
- 容许内部提供回调来解决登录有效,由内部去解决相应的业务逻辑
下面封装基于大前提就是,各个业务零碎后端规范是一样。
通过简略案例应用
// 创立实力 | |
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
拦截器也是采纳经典的 洋葱模型
,如下图所示
为什么要采纳洋葱模型?洋葱模型有什么益处。这里我把本人了解说下(仅是集体了解)
- 分层模式,让每个拦截器专一于做一件事件。
- 申请干涉:能够在申请达到核心(解决业务逻辑之前),增加一些通用解决,比方鉴权、对立参数解决等。这应该就是后端说的切面编程
- 响应干涉:同理对于响应,能够在响应返回给客户端之前,对后果进行解决。(比方:后端返回胜利状态码为 200,但业务零碎应用 SUCCESS,此时在不扭转业务零碎和后端状况,通过拦截器去解决这个问题。)
- 可插拔式
拦截器执行程序
能够先看看外围源码局部:
// 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("执行谬误")
,此时上一次promise
曾经状态从padding
->resolve
,这就是为什么不会进入到reject1
的起因。 - 当
throw
一个谬误,尽管没显示返回新的promise
时,然而主动包装成 ·Promise.reject(Error('执行谬误')
,也就是会执行到reject3
起因。 - 为什么不执行
catch
? 因为谬误并没持续抛出(也就是传递)
弄懂这里之后,再回过头看:
promise = Promise.resolve(config); | |
while (chain.length) {promise = promise.then(chain.shift(), chain.shift()); | |
} |
把下面案例拿进去,当上面代码执行时,失常打印输出:
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
链表应该就能晓得执行程序了。
上面代码运行后的后果:
具体能够理论写一个 DEMO 实操一遍。
总结
- 拦截器采纳经典的洋葱模式
- 拦截器执行程序,
申请拦截器
后退出先执行
,响应拦截器
后退出后执行
。 - 谬误传递在不中断的状况下,执行会装置链上传递。
思考一下
在 chain
中有一个 dispatchRequest
它的用处是啥?