Alfr3d

BlogAboutShort

On Axios Requests & Wrapping

avatar
Alfxjx

why axios?

前后端交互最常见的就是 http 请求,为了提高效率,需要对 http 请求进行封装,目前的现代开发过程中,可以使用 Axios,一种对于 http 请求的封装,或者是fetch,全新的异步请求api,本文主要是介绍我们项目中是如何根据后端返回的类型,对请求进行封装。

Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.jsapi 简单,返回一个 Promise 对象,以供异步的处理。 Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。 事实上两种都可以,相信你看了这篇文章之后也可以自己封装一下 fetch,甚至可以使用适配器模式去一统两种 api。那下面就看看是怎么对 axios 进行封装的。

Requests

首先先看看实际生产中,axios 需要做什么工作:

拦截器,错误处理

axios 在使用的过程中需要生成一个 axiosBase 实例,从开始发送请求到收到响应可以分成以下几个过程:

  1. 发起请求 axiosInstance.get()
  2. 进入请求拦截器 axiosBase.interceptors.request.use(...requestIntercepter);
  3. (server) 服务端进行响应
  4. 进入响应拦截器 axiosBase.interceptors.response.use(responseIntercepter);
  5. 返回响应,在业务中进行使用。

可以看出除了请求和响应之外,axios 提供的最多的配置就是请求拦截和响应拦截,程序设计的目的就是写出可维护并且能复用的代码,因此在两个拦截器中类似管道做通用的处理。

请求和响应

  1. 业务中常见的有 GET/POST/PUT 请求,post 请求又会根据 content-type 分成两种,针对这些变化的量,锚定住代码中不变的量,需要进行设计。
  2. 在常见的业务中,可能会是使用 access-token 的方式进行鉴权,在请求的拦截器中,可以拿到 config 参数,可以添加认证信息
  3. 对于返回的响应报文,由于一般的返回报文是一样的,在响应拦截器中对响应进行第一步的通用处理,减少业务端的重复代码。

业务异常 VS Http 异常

响应拦截中,最常见的就是对异常情况进行处理,由于 axios 返回的是一个 Promise对象,因此要对返回的结果进行处理判断,之后返回 Promise.reject / Promise.resolve; 对于 http 的异常来说,由于本身就是一个 error,一般会放在 Promise.catch 里面去处理。

Wrapping

生成实例

一般来说 baseURL 是不太会改变的,如果项目如果是比较稳定的话,可以把全局的设置也写上,如: withCredentials

axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.withCredentials = true;
const baseURL = NODE_ENV === 'development' ? '/api' : VUE_APP_PROD_API;

// 基本的axios实例
const axiosBase = axios.create({
  baseURL: baseURL,
});

如果项目中依赖多个 api,那么这里可以生成多个实例,配置不同的 baseURL (开发中需要配置对于的 proxies).

// next.js 之类的 jamstack,可以自己生成 api routes 的,具有不同的 backend
const axioRoutes = axios.create({
  baseURL: '/api-routes',
});

拦截器

拦截器的主要功能就是对请求和响应进行处理,包装,最常见的就是附带 token 进行鉴权的操作:

axiosBase.interceptors.request.use((config: AxiosRequestConfig) => {
  const token = sessionStorage.getItem('token');
  if (token && config.headers) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
axiosBase.interceptors.response.use(
  (res: AxiosResponse<IResponse<any>>) => {
    if (!res) {
      return false;
    }
    if (Object.prototype.hasOwnProperty.call(res.data, 'token')) {
      sessionStorage.setItem('token', res.data.token as string);
    }
    return res;
  },
  (err: AxiosError<{ errorMessage: string; success: boolean }>) => {
    // handle the error
    return Promise.reject(err);
  }
);

处理异常

请求一个很重复的操作就是处理异常,一般来说异常都很有规律性,可分成业务操作错误导致的业务异常和由于请求失败导致的 HTTP 异常。

业务异常

AxiosInstance 会返回一个 Promise,对于业务异常都是在 http returnCode 为 200 的时候。以 POST 请求为例子:

定义一个 标准的返回体:

export interface IResponse<T> {
  data: T;
  errorMessage: string;
  success: boolean;
  token?: string;
}

当返回 success 为 false 的时候,表示出现了业务异常。 由于是 Promise.then,可以在拦截器里面进行处理,也可以在实例返回中进行处理,这个地方如果不同的请求方法处理方式不同,就放到对应请求的实例里面去处理,反之就存在拦截器就可以。一般来说不同的请求方式返回的应是一致的:

axiosBase.interceptors.response.use(
  (res: AxiosResponse<IResponse<any>>) => {
    if (!res) {
      return false;
    }
    if (!res.data.success) {
      notify.warning(res.data.errorMessage);
    }
    // token...
  },
  (err: AxiosError<{ errorMessage: string; success: boolean }>) => {
    notify.error(err.response?.data.errorMessage as string);
    if (err.response?.status === 401 && isNoAuth(window.location.pathname)) {
      setTimeout(() => {
        window.location.href = '/person/login';
      }, 1000);
    }
    return Promise.reject(err);
  }
);

http error

对于 http 的异常,为了能够在封装中对其进行统一处理,需要对实例返回的 Promise 进行二次封装,还是以 POST 请求为例:

const httpFuncs = {
  post<T>(
    url: string,
    data: any,
    config?: AxiosRequestConfig
  ): Promise<AxiosResponse<IResponse<T>>> {
    return new Promise((resolve, reject) => {
      axiosInstance
        .post(url, data, config)
        .then((res: AxiosResponse<IResponse<T>>) => resolve(res))
        // http error
        .catch((err: AxiosError<IErrorProps>) => {
          console.log(err.response);
        });
    });
  },
};

Promise.catch中,处理返回的 http error, 主要是 401 和其他的 500 错误,这样就无需在具体的业务中关心这些异常的处理了。

More

到此,基本就是完成了对 axios 等请求库的常规封装。

设计模式?

或许可以使用一个 httpFactory 来对 http 请求进行统一的管理,这样就不会在从 mock 升级到正式的情况都时候,要到每一个实例里面去修改了。

enum enumType {
  BASE,
  MOCK,
}

export class HttpFactory {
  public static getHttp(type: enumType) {
    switch (type) {
      case enumType.BASE:
        return http;
      case enumType.MOCK:
        return httpMock;
      default:
        return http;
    }
  }
}

使用的时候就直接使用 HttpFactory.getHttp(enumType.MOCK).post<IUserInfo>('/userinfo'), 这样升级的时候,直接把枚举修改一下即可。

实际生产中,可以使用配置文件来写这个枚举,这样就可以做到统一的管理。

End

在线演示

总的来说最适合应用的才是最好的,本文只是介绍了我们在改造为 ts + axios + next.js 时候的经验,事实上对于 next.js,可以使用 useSWR 等库,也进行了很好的封装。

References

  1. 比较 fetch()和 Axios
  2. 完整的 Axios 封装-单独 API 管理层、参数序列化、取消重复请求、Loading、状态码...
  3. 封装 Axios 只看这一篇文章就行了
  4. 错误处理 - 最后的完善
  5. vue+ts 下对 axios 的封装
  6. TS 泛型接口
  7. 如何使用装饰器模式极大地增强 fetch()
AxiosNext.jsTypeScript