求star

开源不易,喜欢请点个star吧

Ocean Han
3005 字
15 分钟
在项目中封装Axios
2023-04-24
TIP

​ 转载链接: 在项目中用ts封装axios,一次封装整个团队受益

基础封装#

TIP

​ 我们首先将其封装成一个最基础的Request类,类可以创建多个实例,适用范围更广,封装性更强

// index.ts
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';

class Request {
  // axios实例
  instance: AxiosInstance;

  constructor(config: AxiosRequestConfig) {
    this.instance = axios.create(config);
  }
  request(config: AxiosRequestConfig) {
    return this.instance.request(config);
  }
}

export default Request

拦截器的封装#

TIP

这里我们将拦截器分为三种:

  • 类拦截器(全局请求拦截器)
  • 实例拦截器
  • 接口拦截器

1. 类拦截器#

TIP

​ 类拦截器的实现很简单,直接在类中对axios实例调用interceptors下的两个拦截器:

​ 我们在全局响应拦截器(类拦截器)中做了简单处理,将请求结果中的data返回

import axios, { AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';

class Request {
  // axios实例
  instance: AxiosInstance;

  constructor(config: AxiosRequestConfig) {
    this.instance = axios.create(config);
    this.instance.interceptors.request.use(
      (res: AxiosRequestConfig) => {
        console.log('全局请求拦截器');
        return res;
      },
      (err: any) => {
        return err;
      }
    );
    this.instance.interceptors.response.use(
      (res: AxiosResponse) => {
        // 项目的接口数据都在res.data下,所以我们可以直接返回res.data
        console.log('全局响应拦截器');
        return res.data;
      },
      (err: any) => {
        return err;
      }
    );
  }
  request(config: AxiosRequestConfig) {
    return this.instance.request(config);
  }
}

export default Request;

2. 实例拦截器#

TIP

​ 实例拦截器的存在是为了保证封装的灵活性,因为每一个实例中拦截后处理的操作可能是不一样的,所以我们应该在定义实例时,允许传入拦截器

​ 首先,定义一下interface,方便后面的类型使用和代码提示:

// types.ts
import type { AxiosResponse, AxiosRequestConfig } from 'axios';

export interface RequestInterceptors<T> {
  // 请求拦截
  requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
  requestInterceptorsCatch?: (err: any) => any;
  // 响应拦截
  responseInterceptors?: (config: T) => T;
  responseInterceptorsCatch?: (err: any) => any;
}
// 自定义传入的参数
export interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: RequestInterceptors<T>
}
TIP

​ 既然要允许实例传入拦截器,我们就要对传入的参数类型做一下改造,因为axio提供的AxiosRequestConfig是不允许传入拦截器的,所以我们在上面/types.ts中自定义了RequestConfig,让其继承AxiosRequestConfig达到目的。

​ 修改入参的类型,添加拦截器对象interceptorsObj,使用实例拦截器:

​ 完成之后,拦截器的执行顺序为: 实例请求→类请求→实例响应→类响应

import axios, { AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import type { RequestInterceptors, RequestConfig } from './types/types';

class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors<AxiosResponse>;

  constructor(config: RequestConfig) {
    this.instance = axios.create(config);
    this.interceptorsObj = config.interceptors;
    this.instance.interceptors.request.use(
      (res: AxiosRequestConfig) => {
        console.log('全局请求拦截器');
        return res;
      },
      (err: any) => {
        return err;
      }
    );

    // 使用实例拦截器
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch
    )
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch
    )
    // 全局响应拦截器保证最后执行
    this.instance.interceptors.response.use(
      (res: AxiosResponse) => {
        // 项目的接口数据都在res.data下,所以我们可以直接返回res.data
        console.log('全局响应拦截器');
        return res.data;
      },
      (err: any) => {
        return err;
      }
    );
  }
  request(config: RequestConfig) {
    return this.instance.request(config);
  }
}

export default Request;

3. 接口拦截器#

TIP

​ 那么最后,我们对单一接口进行拦截操作,刚才我们将AxiosRequestConfig类型修改为RequestConfig允许传递拦截器;然后我们又在类拦截器中将接口请求的数据进行了返回,也就是说在request()方法中得到的类型就不是AxiosResponse类型了

​ 查看axios的index.d.ts声明文件,对request()方法的类型定义如下:

// index.d.ts
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
TIP

​ 也就是说它允许我们传递类型,从而改变request方法的返回值类型 :

import axios, { AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import type { RequestInterceptors, RequestConfig } from './types/types';

class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors<AxiosResponse>;

  constructor(config: RequestConfig) {
    this.instance = axios.create(config);
    this.interceptorsObj = config.interceptors;
    this.instance.interceptors.request.use(
      (res: AxiosRequestConfig) => {
        console.log('全局请求拦截器');
        return res;
      },
      (err: any) => {
        return err;
      }
    );

    // 使用实例拦截器
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch
    );
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch
    );
    // 全局响应拦截器保证最后执行
    this.instance.interceptors.response.use(
      (res: AxiosResponse) => {
        // 项目的接口数据都在res.data下,所以我们可以直接返回res.data
        console.log('全局响应拦截器');
        return res.data;
      },
      (err: any) => {
        return err;
      }
    );
  }
  request<T>(config: RequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 如果为单个请求设置拦截器,这里使用单个请求的拦截器
      if (config.interceptors?.requestInterceptors) {
        config = config.interceptors.requestInterceptors(config);
      }
      this.instance.request<any, T>(config)
      .then((res) => {
        // 如果给单个响应设置了拦截器,这里使用单个响应的拦截器
        if (config.interceptors?.responseInterceptors) {
          res = config.interceptors.responseInterceptors(res);
        }
        resolve(res);
      })
      .catch((err: any) => {
        reject(err)
      })
    });
  }
}

export default Request;

取消请求的封装#

TIP

​ 首先定义两个集合,分别用来存放请求url和对应的取消方法:

class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors<AxiosResponse>;
  /*
  存放取消方法的集合
  * 在创建请求后将取消请求方法 push 到该集合中
  * 封装一个方法,可以取消请求,传入 url: string|string[]  
  * 在请求之前判断同一URL是否存在,如果存在就取消请求
  */
  cancelRequestSourceList?: CancelRequestSource[];
  /*
  存放所有请求URL的集合
  * 请求之前需要将url push到该集合中
  * 请求完毕后将url从集合中删除
  * 添加在发送请求之前完成,删除在响应之后删除
  */
  requestUrlList?: string[];

  constructor(config: RequestConfig) {
    //  数据初始化
    this.requestUrlList = [];
    this.cancelRequestSourceList = []; // 取消请求列表
    // ... 省略
  }
}

CancelRequestSource接口的定义如下:

//  types.ts
export interface CancelRequestSource {
  [index: string]: () => void;
}
//  这里的key是不固定的,因为我们使用url做key,只有在使用的时候才知道url,所以这里使用这种语法

1. 取消请求方法的添加和删除#

TIP

​ 改造request方法:

  • 在请求之前,将url和取消请求的方法分别添加到前面我们定义的两个集合当中
  • 在请求完毕之后(无论成功还是失败),将其删除
class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors<AxiosResponse>;
  /*
  存放取消方法的集合
  * 在创建请求后将取消请求方法 push 到该集合中
  * 封装一个方法,可以取消请求,传入 url: string|string[]  
  * 在请求之前判断同一URL是否存在,如果存在就取消请求
  */
  cancelRequestSourceList?: CancelRequestSource[];
  /*
  存放所有请求URL的集合
  * 请求之前需要将url push到该集合中
  * 请求完毕后将url从集合中删除
  * 添加在发送请求之前完成,删除在响应之后删除
  */
  requestUrlList?: string[];

  constructor(config: RequestConfig) {
      //...省略 
  }

  request<T>(config: RequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 如果为单个请求设置拦截器,这里使用单个请求的拦截器
      if (config.interceptors?.requestInterceptors) {
        config = config.interceptors.requestInterceptors(config);
      }
      const url = config.url;
      // url存在 保存当前请求url 和 取消请求方法
      if (url) {
        this.requestUrlList?.push(url);
        // TODO 在axios0.22起,对CancelToken已经弃用,需要改成  AbortController 文档:https://axios-http.com/docs/cancellation
        config.cancelToken = new axios.CancelToken((c) => {
          this.cancelRequestSourceList?.push({
            [url]: c
          });
        });
      }
        this.instance
      .request<any, T>(config)
      .then(res => {
        // 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
        if (config.interceptors?.responseInterceptors) {
          res = config.interceptors.responseInterceptors<T>(res)
        }

        resolve(res)
      })
      .catch((err: any) => {
        reject(err)
      })
      .finally(() => {
        url && this.delUrl(url)
      })
     // ... 省略
  }
}
TIP

上面的代码中,我们将删除请求和其对应的取消方法的逻辑抽离出来,封装成一个函数 :

/**
 * @description: 获取指定 url 在 cancelRequestSourceList 中的索引
 * @param {string} url
 * @returns {number} 索引位置
 */
private getSourceIndex(url: string): number {
  return this.cancelRequestSourceList?.findIndex(
    (item: CancelRequestSource) => {
      return Object.keys(item)[0] === url
    },
  ) as number
}
/**
 * @description: 删除 requestUrlList 和 cancelRequestSourceList
 * @param {string} url
 * @returns {*}
 */
private delUrl(url: string) {
  const urlIndex = this.requestUrlList?.findIndex(u => u === url)
  const sourceIndex = this.getSourceIndex(url)
  // 删除url和cancel方法
  urlIndex !== -1 && this.requestUrlList?.splice(urlIndex as number, 1)
  sourceIndex !== -1 &&
    this.cancelRequestSourceList?.splice(sourceIndex as number, 1)
}

2. 取消请求的方法#

取消全部请求:#

// 取消全部请求
cancelAllRequest() {
  this.cancelRequestSourceList?.forEach(source => {
    const key = Object.keys(source)[0]
    source[key]()
  })
}

根据传入的值执行不同操作(取消一个或多个请求):#

cancelRequest(url: string | string[]) {
  if (typeof url === 'string') {
    // 取消单个请求
    const sourceIndex = this.getSourceIndex(url)
    sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][url]()
  } else {
    // 存在多个需要取消请求的地址
    url.forEach(u => {
      const sourceIndex = this.getSourceIndex(u)
      sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][u]()
    })
  }
}

完整代码示例:#

TIP

最终拦截器的执行顺序将是: 接口请求 -> 实例请求 -> 全局请求 -> 实例响应 -> 全局响应 -> 接口响应

// index.ts
import axios, { AxiosResponse } from 'axios';
import qs from 'qs';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import type { RequestInterceptors, CancelRequestSource, RequestConfig } from './types/types';

class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors<AxiosResponse>;
  /*
  存放取消方法的集合
  * 在创建请求后将取消请求方法 push 到该集合中
  * 封装一个方法,可以取消请求,传入 url: string|string[]  
  * 在请求之前判断同一URL是否存在,如果存在就取消请求
  */
  cancelRequestSourceList?: CancelRequestSource[];
  /*
  存放所有请求URL的集合
  * 请求之前需要将url push到该集合中
  * 请求完毕后将url从集合中删除
  * 添加在发送请求之前完成,删除在响应之后删除
  */
  requestUrlList?: string[];

  constructor(config: RequestConfig) {
    this.requestUrlList = [];
    this.cancelRequestSourceList = []; // 取消请求列表
    this.instance = axios.create(config); //  创建axios实例
    this.interceptorsObj = config.interceptors;
    // 拦截器执行顺序:接口请求 -> 实例请求 -> 全局请求 -> 实例响应 -> 全局响应 -> 接口响应
    this.instance.interceptors.request.use(
      (res: AxiosRequestConfig) => {
        console.log('res', res);
        return res;
      },
      (err: any) => err
    );
    // 使用实例拦截器
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch
    );
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch
    );
    // 全局响应拦截器保证最后执行
    this.instance.interceptors.response.use(
      // 接口的数据都在res.data下,所以直接返回res.data
      (res: AxiosResponse) => {
        return res.data;
      },
      (err: any) => {
        return {
          status: 500,
          message: err
        };
      }
    );
  }
  /**
   * @description: 获取指定 url 在 cancelRequestSourceList 中的索引
   * @param {string} url
   * @returns {number} 索引位置
   */
  private getSourceIndex(url: string): number {
    return this.cancelRequestSourceList?.findIndex((item: CancelRequestSource) => {
      return Object.keys(item)[0] === url;
    }) as number;
  }
  /**
   * @description: 删除 requestUrlList 和 cancelRequestSourceList
   * @param {string} url
   * @returns {*}
   */
  private delUrl(url: string) {
    const urlIndex = this.requestUrlList?.findIndex((u) => u === url);
    const sourceIndex = this.getSourceIndex(url);
    // 删除url和cancel方法
    urlIndex !== -1 && this.requestUrlList?.splice(urlIndex as number, 1);
    sourceIndex !== -1 && this.cancelRequestSourceList?.splice(sourceIndex, 1);
  }
  request<T>(config: RequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 如果为单个请求设置拦截器,这里使用单个请求的拦截器
      if (config.interceptors?.requestInterceptors) {
        config = config.interceptors.requestInterceptors(config);
      }
      const url = config.url;
      // url存在 保存当前请求url 和 取消请求方法
      if (url) {
        this.requestUrlList?.push(url);
        // TODO 在axios0.22起,对CancelToken已经弃用,需要改成  AbortController 文档:https://axios-http.com/docs/cancellation
        config.cancelToken = new axios.CancelToken((c) => {
          this.cancelRequestSourceList?.push({
            [url]: c
          });
        });
      }
      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 如果给单个响应设置拦截器,这里使用单个响应的拦截器
          if (config.interceptors?.responseInterceptors) {
            res = config.interceptors.responseInterceptors(res);
          }
          resolve(res);
        })
        .catch((err: any) => {
          reject(err);
        })
        .finally(() => {
          url && this.delUrl(url);
        });
    });
  }
  // 取消请求
  cancelRequest(url: string | string[]) {
    if (typeof url === 'string') {
      //  取消单个请求
      const sourceIndex = this.getSourceIndex(url);
      sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][url]();
    } else {
      // 存在多个需要取消请求的地址
      url.forEach((u) => {
        const sourceIndex = this.getSourceIndex(u);
        sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][u]();
      });
    }
  }
  // 取消全部请求
  cancelAllRequest() {
    this.cancelRequestSourceList?.forEach((source) => {
      const key = Object.keys(source)[0];
      source[key]();
    });
  }
}

export default Request;

export { RequestConfig, RequestInterceptors };

interface定义:

// types.ts
import type { AxiosRequestConfig, AxiosResponse } from 'axios';

export interface RequestInterceptors<T> {
  // 请求拦截
  requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
  requestInterceptorsCatch?: (err: any) => any;
  // 响应拦截
  responseInterceptors?: (config: T) => T;
  responseInterceptorsCatch?: (err: any) => any;
}

// 自定义传入的参数
export interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: RequestInterceptors<T>;
}
export interface CancelRequestSource {
  [index: string]: () => void;
}

使用方法示例:#

TIP

​ 具体的封装方法和使用方法需要结合项目需求灵活调整

import CONFIG from '@/config/index';
import Request from './index';
import appStore from '@/store';
import { AxiosResponse } from 'axios';

const http = new Request({
  baseURL: CONFIG.serverAddress,
  timeout: 1000 * 60 * 5,
  interceptors: {
    // 请求拦截器
    requestInterceptors: (config) => {
      config.validateStatus = (status) => {
        switch (status) {
          case 401:
            ElMessage.error('用户信息过期或无权限,请重新登录');
            const { saveToken } = appStore.useTokenStore;
            const { saveUserInfo } = appStore.useUserInfoStore;
            const { setUuid } = appStore.useRefreshStore;
            const router = useRouter();
            saveToken(''); //  清除token
            saveUserInfo(''); //  清除用户信息
            setUuid(); //  全局刷新
            router.push('/');
            break;
          default:
            break;
        }
        return status >= 200 && status < 400;
      };
      return config;
    },
    // 响应拦截器
    responseInterceptors: (result: AxiosResponse) => {
      return result
    }
  }
});
export default http;
在项目中封装Axios
https://blog.oceanh.top/posts/frontend/在项目中使用ts封装axios/
作者
Ocean Han
发布于
2023-04-24
许可协议
CC BY-NC-SA 4.0
最后修改时间
2024-08-10 10:08:49