因为须要基于 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.tsimport { 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.tsimport { 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.tsenum 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

为什么会是这样,再思考一下:

  1. 首先 promise 状态流转是不可逆的,也就是只能从 padding -> resolve | reject.
  2. 进入到 resolve1 时执行 throw new Error("执行谬误"),此时上一次 promise 曾经状态从padding -> resolve,这就是为什么不会进入到 reject1 的起因。
  3. throw 一个谬误,尽管没显示返回新的 promise 时,然而主动包装成 ·Promise.reject(Error('执行谬误'),也就是会执行到 reject3 起因。
  4. 为什么不执行 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 实操一遍。

总结

  1. 拦截器采纳经典的洋葱模式
  2. 拦截器执行程序,申请拦截器后退出先执行响应拦截器后退出后执行
  3. 谬误传递在不中断的状况下,执行会装置链上传递。

思考一下

chain 中有一个 dispatchRequest 它的用处是啥?