前端axios

253 阅读8分钟

1. 响应数据的约束和获取

1.1 约束响应数据

axios所提供的常用的请求方法有:

axios.request(config)
axios.get(url[, config])
axios.post(url[, data[, config]])
复制代码

我们来看一下 Axios源码 中对这些请求方法的声明类型,如下所示:

export class Axios {
  request<T = any, R = AxiosResponse<T>, D = any>(
    config: AxiosRequestConfig<D>
  ): Promise<R>;
  get<T = any, R = AxiosResponse<T>, D = any>(
    url: string,
    config?: AxiosRequestConfig<D>
  ): Promise<R>;
  post<T = any, R = AxiosResponse<T>, D = any>(
    url: string,
    data?: D,
    config?: AxiosRequestConfig<D>
  ): Promise<R>;
  // delete,put,options这些方法的声明类型都和上面的大同小异,这里就省略不展示了....
}
// 其中上面AxiosResponse的声明类型如下所示
export interface AxiosResponse<T = any, D = any> {
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}
复制代码

这里我们注意到其中的泛型变量R可用于定义返回数据的类型,其默认为AxiosResponse<T>,而AxiosResponse<T>的泛型变量T用于定义其中的属性data。借助该类型声明,我们可以这样编写请求函数:

/**
 * @description 这里的请求函数用到的接口中:
 *    url为'/admins-no-wrapper',
 *    响应数据格式为{ admins: string[] }
 */
async function getAdmins() {
  const { data } = await instance.get<{ admins: string[] }>(
    "/admins-no-wrapper"
  );
  return data;
}

// 调用函数,展示如何调用请求函数getAdmins
async function requestAdmins() {
  const { admins } = await getAdmins();
  setAdmins(admins);
}
复制代码

从下图的冒泡弹框可知,ts已经自动推导getAdmins的返回数据为Promise<admins: string[]>,这就是借助axios已有泛型来约束响应数据类型的写法。

image.png

这样子,我们就解决了通过ts去约束响应数据的格式

1.2 规范返回数据

针对上面的请求函数,我们还考虑两种情况:

  1. 当请求接口报错时,需要返回错误给开发者处理。
  2. 部分场景下,开发者需要获取Response对象里的部分属性(例如响应头的部分字段值、http 状态码),以实现一些复杂的功能。

对于上面的情况,我们完善一下getAdmins函数:

// 这种写法下的getAdmins有两种返回情况:
//  1. 无报错时:Promise<{err: null, data: {admins: string[]},response: AxiosResponse }>
//  2. 报错时:Promise<{err: err, data: null, response: null }>
async function getAdmins() {
  // 用try~catch来捕捉错误
  try {
    const response = await instance.get<{ admins: string[] }>(
      "/admins-no-wrapper"
    );
    const { data } = response;
    return { data, err: null, response };
  } catch (err) {
    return { data: null, err, response: null };
  }
}

// 当我们调用getAdmins,可用以下写法
async function requestAdmins() {
  // 这里不需要用到response,所以就没解构出response出来
  const { err, data } = await getAdmins();
  // 处理错误
  if (err) {
    // 此时data为null值,因此直接return跳出函数,避免继续执行下面的逻辑导致报错
    return;
  }
  // 处理数据
  // 注意data在这一步里是真值而非null,但data的ts声明类型中包含null值
  // 因此用!强制声明该变量不会是null和undefined
  setAdmins(data!.admins);
}
复制代码

但如果每次写请求函数,我们都要写重复的try~catch逻辑,就会非常累赘。为了减少重复代码的工作量和统一返回的数据格式,我们可以编写一个makeRequest高阶函数,用来生成getAdmins这类请求函数,如下所示:

// RequestConfig用来修饰下面的config,以让url为必填项
interface RequestConfig extends AxiosRequestConfig {
  url: NonNullable<AxiosRequestConfig["url"]>;
}

const instance = axios.create({
  timeout: 10000,
  baseURL: "/api",
});

const makeRequest = <T>(config: RequestConfig) => {
  return async () => {
    try {
      const response = await instance.request<T>(config);
      const { data } = response;
      return { data, err: null, response };
    } catch (err) {
      return { data: null, err, response: null };
    }
  };
};

/** 调用makeRequest,只需要做两步:
 *   1. 在泛型参数T中定义响应数据的类型
 *   2. 在形参中传入config以定义url和method,若method为get则可缺省
 */
const getAdmins = makeRequest<{ admins: string[] }>({
  url: "/admins-no-wrapper",
});

// 和之前的调用方式一样
async function requestAdmins() {
  const { data, err } = await getAdmins();
  if (err) return;
  setAdmins(data!.admins);
}
复制代码

上面例子中,请求函数统一用makeRequest来生成,从而高效准确实现了规范请求函数的返回数据

1.3 处理后端返回的响应对象

在大多数实际开发中,后端不会把响应数据直接放在响应体里,而是放在一个对象(下称返回对象)的其中一个属性里,然后把返回对象放在响应体里返回给前端。返回对象往往包含三个大同小异的属性,分别如下所示:

image.png

  • code: number类型,用于存放状态代码,有时候 http 的状态码不能完全满足业务场景需求,需要额外定义一些状态码来说明请求失败的情况。这里设计 code 为 0 时代表请求通过。
  • data: 用于存放数据
  • message: string类型,用于存放额外信息。当 code 不为 0 时,需要 message 来提供更多说明信息以反馈给用户和开发者。

对此,我们还需要完善makeRequest来解构处理返回对象,如下所示:

// 先定义返回对象的声明类型
export interface BackendResultFormat<T = any> {
  code: number;
  data: T;
  message: string;
}

// 定义一个返回对象中code不为0时的错误
export class CodeNotZeroError extends Error {
  code: number;

  constructor(code: number, message: string) {
    super(message);
    this.code = code;
  }
}

const makeRequest = <T>(config: RequestConfig) => {
  return async () => {
    try {
      const response = await instance.request<BackendResultFormat<T>>(config);
      const res = response.data;
      // 当返回对象的code不为0时,生成CodeNotZeroError的实例存放到err里返回出去
      if (res.code !== 0) {
        const error = new CodeNotZeroError(res.code, res.message);
        return { err: error, data: null, response };
      }
      return { err: null, data: res.data, response };
    } catch (err) {
      return { data: null, err, response: null };
    }
  };
};
复制代码

这样子就完成了处理返回对象的逻辑。

2. 请求参数的传入和约束

2.1 请求参数的传入

AxiosRequestConfig声明类型里的属性中,可以按确定时期分为两类:

  1. 配置属性:这部分参数在在调用makeRequest生成请求函数时就可以确定下来,例如method,url。之前的RequestConfig声明类型中会把配置属性定义为必填项。
  2. 调用属性:这部分参数在调用请求函数时可以确定下来,例如data,param
export interface AxiosRequestConfig<D = any> {
  // 配置属性
  url?: string;
  method?: Method | string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  // ....

  // 调用属性
  params?: any;
  data?: D;
  cancelToken?: CancelToken; //用于中断上一次重复请求,0.27版本推荐用signal来代替该属性
  signal?: GenericAbortSignal; //用于中断上一次重复请求
  onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; // 上传回调
  onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void; // 下载回调
  // ...
}
复制代码

当然有些属性可能同时属于配置属性调用属性,例如headers,timeout

而本节中要在请求函数中支持传入的那些请求参数就是上面所说的调用属性。为了让定义调用属性时的做法和调用axios.request(config)一致,我们可以指定请求函数的形参为Partial<RequestConfig>声明类型。下面我们按照这个思路来实现:

// config用于指定配置属性,因此用RequestConfig修饰
// requestConfig用于指定调用属性或者覆盖配置属性,因此先用Partial<RequestConfig>修饰,在下面章节中会有所改动
const makeRequest = <T>(config: RequestConfig) => {
  return async (requestConfig?: Partial<RequestConfig>) => {
    // 最终两个config会进行浅合并
    const mergedConfig: RequestConfig = {
      ...config,
      ...requestConfig,
      // 对于headers需要特殊处理做深度合并
      headers: {
        ...config.headers,
        ...requestConfig?.headers,
      },
    };

    // 下面逻辑与之前一样
    try {
      const response = await instance.request<
        BackendResultFormat<T>,
        RequestConfig
      >(mergedConfig);
      const res = response.data;
      if (res.code !== 0) {
        const error = new CodeNotZeroError(res.code, res.message);
        return { err: error, data: null, response };
      }
      return { err: null, data: res.data, response };
    } catch (err) {
      return { data: null, err, response: null };
    }
  };
};

const getNames = makeRequest<{ names: string[] }>({
  url: "/names",
  method: "get",
});

const requestNames = async (search?: string) => {
  const { data, err } = await getNames({
    // 此处params等同于axios.request(config)中的config.params
    params: { search },
  });
  if (err) return;
  setNames(data!.names);
};
复制代码

这样子就实现了请求函数的形参传入

2.2 请求参数的约束

上节中我们简单用Partial<RequestConfig>来修饰请求函数的形参,而RequestConfig继承于AxiosRequestConfig,我们先来看看AxiosRequestConfig这个声明类型:

export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method | string;
  baseURL?: string;
  headers?: AxiosRequestHeaders;
  params?: any;
  data?: D;
  // ..还有很多属性,不过不涉及到本章节分析因此省略
}
复制代码

AxiosRequestConfig声明类型可知,如果直接用Partial<RequestConfig>修饰请求函数的形参有两个缺陷:

  1. config.data的约束不够严谨: 当我们在定义AxiosConfig的泛型参数D时,是对config.data进行类型约束,但由于?:的存在,data在不赋值的情况下不会报出ts的错误
  2. 缺乏对config.params的约束: 部分请求需要传入config.params,因此也需要对config.params进行类型约束,但AxiosConfig没有泛型参数去对此进行约束

为了解决上面存在的问题,我们对makeRequest设计一个类型声明MakeRequest,如下所示:

/**
 * 允许定义三个泛型参数:
 *    Payload为响应数据
 *    Data为请求体参数,对应config.data
 *    Params对应URL的请求参数,对应config.params
 *  对不同泛型值有不同的传参方式
 */
interface MakeRequest {
  // 当不定义泛型或只定义Payload泛型参数时,返回对象data为any或你定义的Payload
  <Payload = any>(config: RequestConfig): (
    requestConfig?: Partial<RequestConfig>
  ) => Promise<Payload>;
  // 当泛型参数Data被定义时,config和config.data不能为空
  <Payload, Data>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, "data">> & { data: Data }
  ) => Promise<Payload>;
  // 当泛型参数Params被定义时,config和config.params不能为空
  // 但如果Data为undefined时,config.data可以不填写
  <Payload, Data, Params>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, "data" | "params">> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) & {
        params: Params;
      }
  ) => Promise<Payload>;
}
复制代码

我们把MakeRequest声明类型加到makeRequest函数上,如下所示:

// 加上MakeRequest声明类型,其余保持不变
const makeRequest: MakeRequest = <T>(config: RequestConfig) => {
  //... 省略
};
复制代码

下面来看一下在MakeRequest的作用下,加上泛型参数生成的请求函数,在调用过程中对请求参数校验的结果:

  • 在只定义Payload的情况下

    image.png

  • 定义PayloadData的情况下

    image.png

  • 定义PayloadDataParam的情况下

    image.png

  • 定义PayloadParam的情况下

    image.png

从上面的效果可知,我们已经完成了对请求参数的格式约束

作者:村上小树
链接:juejin.cn/post/713175…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。