代码见最下面:)

背景

在重构项目的时候,看到小伙伴基于Axios 封装的HttpClient 不合理,params 和 data 混淆,使用起来太费劲。于是我基于项目需求封装了一个 HttpClient。

解决什么问题:

  • 多个接口使用通用参数,在调用时重复传递,完全多余
  • 返回值统一处理, 错误处理和上报
  • 超时 loading
  • 返回值 TypeScript 类型定义和提示

1.多个接口使用通用参数,冗余模版代码

一套接口,通常需要一些通用的参数,比如 token,id,直接存储在一个独立的 client 实例里面能减少很多调用层模版代码。

Axios 使用 params 和 param 属性来设置url上的参数。我们可以使用 commonOption 来存储这些通用的属性,并HttpClient方法内部使用 Object.assign 来merge commonOption和用户定制参数。这样暴露出来的 put post 等方法就和你使用 Axios 完全一毛一样!同时本封装的宗旨就是和Axios保持完全一致的使用方式。

/**
* get
* @method get
* The same as axios.get
*/
public get = (url: string, option?: AxiosRequestConfig): AxiosPromise<any> => {
  return this.axios.get(url, Object.assign({}, this.commonOption, option));
};

配合 Axios.create 实例化的时候传递一个baseURL和timeout,可以让接口调用非常的清爽。

// constructor 第二个参数option会直接存储在实例上,我们把baseURL和timeout存在这里
class HttpClient {
  public commonOption: AxiosRequestConfig;
  public axios: AxiosInstance;

  constructor(commonOption: AxiosRequestConfig, option: AxiosRequestConfig) {
    this.commonOption = commonOption;
    this.axios = Axios.create(option);

    /***
     * 添加默认的响应拦截器,把成功返回且code===0的结果直接返回data
     */
    this.axios.interceptors.response.use(
      (response: AxiosResponse): any => {
        if (response.data && response.data.code === 0) {
          return response.data.data;
        } else if (response.data) {
          return response.data;
        } else {
          return response;
        }
      },
      (error: AxiosError) => {
        return Promise.reject(error);
      }
    );
  }
}

// 使用时,实例化时传递两个参数
const client = new NewHttpClient(
  {
    withCredentials: true,
    params: {
      id: 'dummy-id',
      token: 'dummy-token'
    }
  },
  {
    baseURL: config.prefix,
    timeout: 3000
  }
);

2.超时 loading

在请求时,响应超时需要展示 loading 界面以优化用户体验。我们可以使用 Axios 的拦截器来解决这个问题。

Axios 的拦截器是一个中间件,你可以理解为一个处理请求和响应的插件。所以你可以设置多个插件,以满足实现不同的功能。

Axios 的拦截器分为 请求拦截器和响应拦截器。我们看看官方的拦截器使用示例:

// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Do something with response data
    return response;
  }, function (error) {
    // Do something with response error
    return Promise.reject(error);
  });

而 Axios 的 interceptors 类型和函数签名如下:

export interface AxiosInstance {
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
}

export interface AxiosInterceptorManager<V> {
  use(onFulfilled?: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
  eject(id: number): void;
}

两个拦截器配合使用能很好的解决我们的问题:在请求拦截器中添加定时器,超时则显示 Loading,在响应拦截器中隐藏该 Loading。

那么我们需要做的就是添加两个拦截器:一个请求拦截器,负责检测超时显示 loading;一个响应拦截器,负责隐藏loading(请求成功后隐藏拦截器,失败时显示错误并隐藏拦截器)。

使用方式:

function showLoaing () {
  // show loading
}
function hideLoaing () {
  // hide loading
}

new httpClient = new HttpClient(...);

httpClient.setRequestInterceptor(
  (config) => {
    showLoaing()
  }, 
  (error) => {}
)

httpClient.setResponseInterceptor(
  (response) => {
    hideLoaing()
  }, 
  (error) => {}
)

3.统一处理返回值

restful 请求的返回值一般都有一定的格式,比如下面这种格式:

// 成功
const ResponseSuccess = JSON.strinify({
  code: 0,
  data: "{} || [] || """
})
// 失败
const ResponseFailed = JSON.strinify({
  code: "errorCode",
  data: {
    errorMessage: "错误原因"
  }
})

请求成功时,我们只关心返回的数据;请求失败时,我们需要提示用户,并且上报错误。

axiosInstance.interceptors.response.use(
  // 请求成功
  (response: AxiosResponse): any => {
    if (response.data && response.data.code === 0) {
      return response.data.data;
    } 
    else if (response.data) {
      return response.data;
    } 
    else {
      return response;
    }
  },
  // 请求失败
  (error: AxiosError) => {
    const { response } = error;
    // 当server返回 500 时,response 为 undefined
    if (response && response.data && response.data.errorMessage) {
      showToast(response.data.errorMessage);
    }
    // 上报错误
    reportError(error);
    // 返回 error 以提供给下一个interceptor使用 
    return error;
  }
);

4.返回值 TypeScript 类型定义和提示

在 TypeScript 中使用 Axios 的返回值时,如果能直接定义好每个接口的请求返回值那是再棒不过了!这样在业务组件中就不怕和接口返回的不一致导致意外 bug 了。

export interface AxiosInstance {
  get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>;
  delete(url: string, config?: AxiosRequestConfig): AxiosPromise;
  head(url: string, config?: AxiosRequestConfig): AxiosPromise;
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>;
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>;
  patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>;
}

上面代码是 Axios 的各个方法函数签名,皆返回 AxiosPromise<T>,代表返回值是一个 AxiosPromise,T 为 AxiosPromise resolve 之后 then 所获取的数据的类型。但是在上面我们已经设置过拦截器,令response直接返回后端返回的数据,所以方法返回的值应该是data,我们设置为 any。

现在我们已经将 HttpClient 的方法返回值设置成了 any,实际使用中我们只需要给定返回我们想要的格式用 Promise 类型包装一下即可,如下例子:

interface IResponseProfile = {
  name: string;
  id: number;
  age: number;
}

/**
* 获取用户主页
*/
public getMixer = (id: string): Promise<IResponseProfile> => {
  return this.client.get("/api/v1/profile", { params: { id } });
};

现在,我们可以在业务层开心的使用上面的 IResponseProfile 类型提示了。


全部代码在此:

/**
 * 一个基于Axios的HttpClient封装
 * @author stackfizz
 * @description 为了减少通用参数传递,统一返回值处理
 */

import * as qs from "qs";
import Axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from "axios";

export interface IResponseSuccess {
  code: number;
  data: any;
}

export interface IResponseFailed {
  code: number;
  data: any;
}

class HttpClient {
  public commonOption: AxiosRequestConfig;
  public axios: AxiosInstance;

  constructor(commonOption: AxiosRequestConfig, option: AxiosRequestConfig) {
    this.commonOption = commonOption;
    this.axios = Axios.create(option);

    /***
     * 添加默认的响应拦截器,把成功返回且code===0的结果直接返回data
     */
    this.axios.interceptors.response.use(
      (response: AxiosResponse): any => {
        if (response.data && response.data.code === 0) {
          return response.data.data;
        } else if (response.data) {
          return response.data;
        } else {
          return response;
        }
      },
      (error: AxiosError) => {
        return Promise.reject(error);
      }
    );
  }

  // Custom commonOption via merge
  public setCommonOption = (config: AxiosRequestConfig): void => {
    this.commonOption = Object.assign(this.commonOption, config);
  };

  // Return current commonOption
  public getCommonOption() {
    return Object.assign({}, this.commonOption);
  }

  // Set request interceptor
  public setRequestInterceptor = (
    onFulfilled?: (config: AxiosRequestConfig) => AxiosRequestConfig,
    onRejected?: (error: any) => any
  ) => {
    this.axios.interceptors.request.use(onFulfilled, onRejected);
  };

  // Set response interceptor
  public setResponseInterceptor = (
    onFulfilled?: (response: AxiosResponse) => AxiosResponse,
    onRejected?: (error: any) => any
  ) => {
    this.axios.interceptors.response.use(onFulfilled, onRejected);
  };

  // Custom AxiosRequestConfig via merge
  public setRequestConfig = (config: AxiosRequestConfig) => {
    this.axios.defaults = Object.assign(this.axios.defaults, config);
  };

  /**
   * get
   * @method get
   * The same as axios.get
   * 返回值为any,是因为添加了拦截器,返回值为后端的 data
   */
  public get = (url: string, option?: AxiosRequestConfig): any => {
    return this.axios.get(url, Object.assign({}, this.commonOption, option));
  };

  public put = (url: string, data?: any, option?: AxiosRequestConfig): any => {
    return this.axios.put(url, data, Object.assign({}, this.commonOption, option));
  };

  public post = (url: string, data?: any, option?: AxiosRequestConfig): any => {
    return this.axios.post(url, data, Object.assign({}, this.commonOption, option));
  };

  /**
   * postFrom
   * @method postForm
   * post 发送表单的快捷方式
   */
  public postForm = (url: string, data?: any, option?: AxiosRequestConfig): any => {
    return this.post(url, data, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      transformRequest: [
        ({ data }: { data: any }) => {
          return qs.stringify(data);
        }
      ]
    });
  };
}

export default HttpClient;