Axios的封装思想及实践(TS版本)

9,121 阅读6分钟

源于coderWhy教学视频思想,对axios做一个简单的小结,再次感谢coderwhy大神

Axios官网(中文):Axios (axios-http.com)

基本安装

//npm方式
npm install axios
//yarn方式
yarn add axios

基本使用

axios(config)
axios.request(config)
axios.get(config)
axios.post(config)

缺点是耦合度太高,相同配置需多次重复,且不灵活

封装思想

  • 直接使用axios,依赖性太强,如果今后更换网络请求库会很麻烦
  • 一些公共的请求功能,每次请求都需要重写配置
  • aixos进行加一层封装,将axios封装为自定义的request,将来直接使用request来发送网络请求就行,日后想要更换网络请求库,可以直接修改request层,将一些公共的功能封装在request层,如网络请求头添加Authorization(即token),加载loading效果等等,拦截器可以灵活封装

image-20211024150938188

使用Typescript进行封装的一些前置知识梳理

  1. axios(config)config的类型为AxiosRequestConfig,具体见官网:请求配置 | Axios 中文文档 (axios-http.cn)

    常用的有几个:url,method,baseUrl,data,timeout

    默认配置可以进行升级改造,新建一个接口实现AxiosRequestConfig,在其中添加一些新的配置

  2. AxiosResponse---axios默认的返回值类型,接收泛型T默认为any,代表的是实际接收到的返回数据类型,一般会将T设置为IDataType

    //一般情况下我们只使用data
    export interface AxiosResponse<T = any>  {
      data: T;
      status: number;
      statusText: string;
      headers: any;
      config: AxiosRequestConfig;
      request?: any;
    }
    //一般在使用中,我们会将返回数据类型定义为如下形式
    export interface IDataType<T = any> {
      status: string
      msg: string
      data: T
    }
    

    日常使用中,我们不会使用AxiosResponse作为封装后的返回值数据类型,会提取其中的data,可以通过在响应成功拦截器中返回result.data来实现(后面会详细介绍),而data的类型即为IDataType

  3. 封装统一使用原生实例的request方法来进行

    //T默认是any类型,返回值默认是AxiosResponse<T>
    request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
    

    封装后实现的效果如下:

    //T即为IDataType类型,返回的是一个Promise<T>
    xxRequest.get<T>(config: xxRequestConfig<T>): Promise<T>
    

一、封装一(去耦合)

  1. 创建自定义网络请求类JJRequest,将axios实例包装在内

    //service/request/request.ts
    class JJRequest {
      instance: AxiosInstance
    
      constructor(config: AxiosRequestConfig) {
        this.instance = axios.create(config)
      }
    
      //返回的Promise中结果类型为AxiosResponse<any>
      request(config: AxiosRequestConfig): Promise<AxiosResponse> {
        return new Promise<AxiosResponse>((resolve, reject) => {
          this.instance
            .request(config)
            .then((res) => {
              resolve(res)
            })
            .catch((err) => {
              reject(err)
            })
        })
      }
    }
    
  2. 创建实例(仅配置基本的baseURLtimeout,属于实例级别配置)

    //service/index.ts
    //使用环境配置
    const jjRequest = new JJRequest({
      baseURL: process.env.VUE_APP_BASE_URL,
      timeout: process.env.VUE_APP_TIME_OUT
    })
    
  3. 具体使用

    //main.ts
    jjRequest
      .request({
        url: '/xxx'
      })
      .then((res) => {
        console.log(res)
      })
    

查看结果:可以看出结果为AxiosResponse<any>类型

image-20211024185624638

二、封装二(添加全局级别拦截)

原生的拦截器位于axiosInstance实例中,使用方法是(来源官网:拦截器 | Axios 中文文档 (axios-http.cn)

注意:响应成功和响应失败的判别标准,以状态码2xx为界限,超出的响应失败

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

全局拦截在JJRequest的构造函数中实现

//service/request/request.ts
//省略前后部分代码
//....
constructor(config: AxiosRequestConfig) {
  this.instance = axios.create(config)
  
  //全局请求拦截
  this.instance.interceptors.request.use(
      (config) => {
        console.log(config)
        return config
      },
      (error) => {
        console.log('全局请求失败拦截', error)
      }
    )
  //全局响应拦截
    this.instance.interceptors.response.use(
      (res) => {
        //res为AxiosResponse类型,含有config\data\headers\request\status\statusText属性
        console.log(res)
        //改造返回的数据类型,即将AxiosResponse的data返回
        return res.data
      },
      (error) => {
        console.log('全局响应失败拦截')
        console.log(error.request)//
        console.log(error.response)
        return error
      }
    )
  
  //加入泛型限定,返回数据类型为T,
  request<T>(config: AxiosRequestConfig<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      this.instance
        .request<any, T>(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }
}

实际使用时如下

//main.ts==>返回的数据类型限定为IDataType
jjRequest
  .request<IDataType>({
    url: '/xxx'
  })
  .then((res) => {
    console.log(res)
  })
  .catch((err) => {
    console.log('=====', err)
  })

同封装一的接口在main.ts中运行后效果如下,只剩下AxiosResponse中的data,此处为自定义的IDataType类型,暂时还未明确加入泛型,后面会加入

image-20211025195609391

演示响应失败的拦截:(将后端接口中的/xxx改为了/xxxx,则前端访问/xxx将出现响应失败404

image-20211025200928153

三、封装三(自定义实例级别的拦截器,添加token)

需实现的效果如下:

//service/index.ts
const jjRequest = new JJRequest({
  baseURL: process.env.VUE_APP_BASE_URL,
  timeout: process.env.VUE_APP_TIME_OUT,
  //实例级别的拦截器,在创建axios实例的时候携带拦截器
  interceptors:{
    requestInterceptor: ...
    requestInterceptorCatch: ...
    responseInterceptor: ...
    responseInterceptorCatch: ...
  }
})

因为原生的AxiosRequestConfig中没有拦截器配置这个属性,因此需要自定义改造,一是自定义拦截器接口,二是自定义请求配置接口

  1. 自定义拦截器接口(共四个拦截器,四个函数)

    interface IJJRequestInterceptors<T = AxiosResponse> {
      //请求成功
      requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
      //请求失败
      requestInterceptorCatch?: (error: any) => any
      //响应成功
      responseInterceptor?: (res: T) => T
      //响应失败
      responseInterceptorCatch?: (error: any) => any
    }
    
  2. 自定义请求配置接口,可选属性,可配可不配

    interface IJJRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
      interceptors?: IJJRequestInterceptors<T>
    }
    
  3. 改造网络请求类

    class JJRequest {
      instance: AxiosInstance
      //该属性从实例中获取
      interceptors?: IJJRequestInterceptor
    
      constructor(config: IJJRequestConfig) {
        this.instance = axios.create(config)
        //从实例配置config中获取拦截器配置
        this.interceptors = config.interceptors
    
        //全局请求拦截
        this.instance.interceptors.request.use(..., ...)
        //全局响应拦截
        this.instance.interceptors.response.use(..., ...)
    
        //实例级别拦截
    		this.instance.interceptors.request.use(
          this.interceptors?.requestInterceptor,
          this.interceptors?.requestInterceptorCatch
        )
        this.instance.interceptors.response.use(
          this.interceptors?.responseInterceptor,
          this.interceptors?.responseInterceptorCatch
        )
      }
    }
    
  4. 创建实例

    const jjRequest = new JJRequest({
      baseURL: process.env.VUE_APP_BASE_URL,
      timeout: process.env.VUE_APP_TIME_OUT,
      interceptors: {
        requestInterceptor: (config) => {
          //此处token在开发中可从localStorage中获取,token一般从服务器获取存在vuex中,然后转存到localStorage中,自己封装关于localStorage的方法,此处用一个常量代替
          //const token = localCache.getCache('token')
          const token = 'this ia a token'
          if (token) {
            config.headers.Authorization = `Bearer ${token}`
          }
          return config
        }
      }
    })
    

结果展示

image-20211025205705544

四、封装四(单个请求调用级别的拦截)

实现调用级别的拦截,需要在request方法中做文章,将其参数类型也由AxiosRequestConfig升级为IJJRequestConfig,实现的思路是直接调用该方法

//service/request/request.ts
//...
request<T>(config: IJJRequestConfig<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      //请求拦截设置位置
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }

      this.instance
        .request<any, T>(config)
        .then((res) => {
          //响应拦截设置位置
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          resolve(res)
        })
        .catch((err) => {
          console.log('=====', err)
          reject(err)
        })
    })
  }

调用时如下

jjRequest
  .request<IDataType>({
    url: '/xxx',
  //单个请求调用级别单独设置拦截器
    interceptors: {
      //直接返回IDataType类型结果中的data属性项
      responseInterceptor: (res) => res.data
    }
  })
  .then((res) => {
    console.log(res)
  })
  .catch((err) => {
    console.log('=====', err)
  })

结果如下

image-20211025213317041

五、封装五(封装get、post等,方便调用)

 //service/request/request.ts
 //....
 get<T = any>(config: IJJRequestConfig<T>): Promise<T> {
     return this.request<T>({
       ...config,
       method: 'GET'
     })
   }
 ​
   post<T = any>(config: IJJDRequestConfig<T>): Promise<T> {
     return this.request<T>({
       ...config,
       method: 'POST'
     })
   }
   //其余patch、delete等参照一样的,实际调用时可直接使用jjRequest.get<IDataType>(config)来请求数据

axios的封装划分了三层

  • 全局层
  • 实例层
  • 单个请求层

将来可以根据实际情况做相应的封装,主要考虑的是将封装放在哪一层来做,本文主要是写一写封装的思想

例如loading加载效果,可以在全局来做,也可以在实例层来做,也可以在单个请求处来做,看具体需求!此处就不在做具体的封装,仅做抛砖引玉的作用!

补充:拦截的执行顺序

image-20211025220552357


对于axios的封装还有很多,比如关于重复请求的封装、参数序列化等,按需进行即可。ps:主要是我还不会,哈哈。。

来自一个业余程序爱好者小弟的小结,不足之处,请各位大佬指正!