如何基于 axios 封装业务请求库

由于需要基于 axios 封装自己业务请求库,主要解决问题就是把通用操作封装,减少重复操作,同样响应错误码进行集中管理,这样可以更加侧重在业务上的开发.

往往每新开发一个项目都需要去重写或复制老项目,同样当后端新增或修改业务 code 时,都需要 N 个项目做修改和维护。 解决这个问题就是在 axios 基于上封装一层,这层处理通用型的问题和把后端状态进行集中式的管理。

后端的变化之需要从 N端更改,改为一端更改。大大提升开发效率和维护成本。

封装自己的业务插件,做到如下两点:

  • 不增加用户使用成本(使用行为上同 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 拦截器也是采用经典的洋葱模型,如下图所示

如何基于 axios 封装业务请求库_第1张图片

为什么要采用洋葱模型?洋葱模型有什么好处。 这里我把自己理解说下(仅是个人理解)

  • 分层模式,让每个拦截器专注于做一件事情。
  • 请求干预:可以在请求到达中心(处理业务逻辑之前),添加一些通用处理,比如鉴权、统一参数处理等。这应该就是后端说的切面编程
  • 响应干预:同理对于响应,可以在响应返回给客户端之前,对结果进行处理。(比如:后端返回成功状态码为 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);
});

上面代码构建的拦截器链如下图:

如何基于 axios 封装业务请求库_第2张图片

这样结合前面的洋葱图,是不是跟上面箭头指向顺序完全吻合。

看下如下代码:

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);
});

未命名绘图 (2).jpg

如果理解前面简单 promise 案例,对着上面 chain 链表应该就能知道执行顺序了。

下面代码运行后的结果:

如何基于 axios 封装业务请求库_第3张图片

具体可以实际写一个 DEMO 实操一遍。

总结

  1. 拦截器采用经典的洋葱模式
  2. 拦截器执行顺序,请求拦截器后加入先执行响应拦截器后加入后执行
  3. 错误传递在不中断的情况下,执行会安装链上传递。

思考一下

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

你可能感兴趣的:(如何基于 axios 封装业务请求库)