每天对自己多问几个为什么,总是有着想象不到的收获。 一个菜鸟小白的成长之路(copyer)

1、理解promise的类型限定

Promise本身是可以有类型的, 对resolve,reject参数类型限定

new Promise<string>((resolve, reject) => {
  // resolve(321)   报错: number类型
  resolve('james')  //字符串类型
}).then(res => {
  console.log(res)  //这里的res就是字符串的形式
})

2、理解axios提供的几种类型

以get为例

get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;

一:参数

get()是一个函数,接收两个参数,返回一个Promise对象

  • url: 字符串: 路径地址
  • config: 配置对象,类型为AxiosRequestConfig, 看下面

二:返回值的类型

​ 上面解释了Promise的对象,本身是有类型的,类型为 R

​ R是卅呢? 泛型 R = AxiosResponse<T>, 泛型接口, AxiosResponse,看下面

T 也是泛型, 看了AxiosResponse的接口之后,很明显知道 T 就是data的类型

1、对象配置的类型

export interface AxiosRequestConfig {
  url?: string;
  method?: Method;
  baseURL?: string;
  headers?: any;
  params?: any;
  data?: any;
  timeout?: number;
  withCredentials?: boolean;
  responseType?: ResponseType;
  ...
}

2、axios的返回值类型

export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

3、实例

//http.get()返回一个promise对象, promise返回值的类型为 string[] 
http.get<string[]>('http://httpbin.org/get').then(res => {
    // res.data: string[]
})

3、class + ts二次封装axios

在这里主要是使用ES6提供的类来对axios进行封装,在使用的时候,通过实例来进行使用。

1、封装基本类(初级)

封装类:

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

class Http {
    public instance: AxiosInstance
    constructor(config: AxiosRequestConfig) {
    	this.instance = axios.create(config)
    }
}
export default Http

可以根据类来进行创建实例:

import Http from './service/index'

//配置基本路径和请求时间   (当然这里也可以写在配置文件)
const BASE_URL = 'http://127.0.0.1'
const TIME_OUT = 1000

const http = new Http({
  baseURL: BASE_URL,
  timeout: TIME_OUT
})

export default http

2、拦截器配置(中级)

拦截器主要分为以下三种的形式,但是主要的形式,第一种和第三种是不常见的(了解即可),最主要的还是同统一的拦截器(掌握)

  • 针对不同实例之间的进行不同的拦截(了解)
  • 对所有实例进行统一拦截(掌握)
  • 针对不同接口进行不同的拦截(了解)

尽管上面三种情况,两种需要了解,但是还是需要理解他的封装原理

先对ts接口进行实现(接下来会使用到)

//自定义拦截器的类型限定(针对于第一种情况和第三种情况)
interface customInterceptorType {
  //请求拦截(接口请求成功)
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig 
  requestInterceptorCatch?: (error: any) => any     //请求拦截(捕获接口失败)
  resInterceptor?: (res: AxiosResponse) => AxiosResponse    //响应拦截(接口响应成功)
  resInterceptorCatch?: (error: any) => any                 //响应拦截(捕获响应失败)
}

//自定义的axios的config的配置(使用接口继承)
interface customRequest extends AxiosRequestConfig {
  interceptor?: customInterceptorType     //是否传入拦截器
}

对axios中的config的类型,进行重写

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

class Http {
    public instance: AxiosInstance
    constructor(config: customRequest) {   //使用自定义的axios的config的配置
    	this.instance = axios.create(config)
    }
}
export default Http

2.1、不同实例之间的拦截器

不同的实例,其实就是存在两个项目之间。(主要是为了让封装更加的具有扩展性)

使用class封装的axios,通过new关键字进行实例化,来进行使用的。那么创建实例就会存在多个实例,那么他们之间的拦截可能是想要不一样的功能。

样例:

假如存在两个实例, http1http2http1发送请求之前,在url前面加上专属的字段,如:/http1/getSearchDatahttp2在发送之前修改url加上专属的字段,如: http2/getSearchData, 当然这种情况很少见,这里只是简单的举个例子

类实例初始化:

import Http from './request/request'

const http1 = new Http({
     baseURL: BASE_URL,
  	 timeout: TIME_OUT,
     interceptor: {
         requestInterceptor: (config) => {
             config.url = '/http1' + config.url
             return config 
         },
         //其他的三种拦截器可选
     }
})

const http2 = new Http({
     baseURL: BASE_URL,
  	 timeout: TIME_OUT,
     interceptor: {
         requestInterceptor: (config) => {
             config.url = '/http2' + config.url
             return config 
         },
         //其他的三种拦截器可选
     }
})

在上面的代码中,在初始化的时候, 就传入了一个请求拦截器,对url字符串的修改,这样就达到了,对不同的实例之间的不同拦截。

类中处理实例传入的拦截器

class Http {
    public instance: AxiosInstance
    public interceptor?: customInterceptorType
    constructor(config: customRequest) {   
    	this.instance = axios.create(config)
        this.interceptor = config.interceptor
        //如果存在this.interceptor, 就处理实例中传递过来的拦截器
        this.instance.interceptor.request.use(
        	this.interceptor?.requestInterceptor,
            this.interceptor?.requestInterceptorCatch
        )
         this.instance.interceptors.response.use(
            this.interceptor?.resInterceptor,
            this.interceptor?.resInterceptorCatch
         )
    }
}
export default Http

上面代码,就单独处理实例传递过来的拦截器,注入。

2.2 、对所有实例进行统一拦截

这个是非常常用的拦截器,这里可以大部分想要的实现功能拦截,所以我认为这是必须要掌握的。比如:

  • 在请求中,对token的拦截
  • 在请求中,对loading图标的显示
  • 在响应中,对数据的处理

当然,还有很多的情况处理。这样的代码逻辑,也是比较好处理的

class Http {
    public instance: AxiosInstance
    ...
    
    this.instance.interceptors.request.use(
      (config) => {
        //请求拦截:对token的拦截
        const token = localStorage.getItem('token')
        token && config.headers.common['token'] = token
        return config
      },
      (error) => {
        return error
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        //响应拦截: 接口返回的数据处理
        return res.data
      },
      (error) => {
        return error
      }
    )
}

2.3、针对不同接口进行不同的拦截

在某些场景下,需要对个别接口进行单独的拦截处理。那么在封装类中,我们就需要单独对这种拦截器处理。

样例:

​ 针对大部分的接口,请求超时的时间时统一的,但是对于处于复杂的接口,请求的时间肯定会长一些。比如登陆接口,如果时间超出2s,那么就说明请求接口失败。但是如果针对于人脸识别的接口,如果时间在1分钟左右,我们也是可以接受的。所以这种情况下,就需要对接口拦截器进行处理。

类中处理接口拦截器

class Http {
    request(config: customRequest): any {
        if (config.interceptor?.requestInterceptor) {
            //调用接口传递过来的拦截器函数,接收最新的config
            config = config.interceptor.requestInterceptor(config)
    }
    this.instance.request(config).then((res) => {
        if (config.interceptor?.resInterceptor) {
            config = config.interceptor.resInterceptor(res)
    	}
    })
    }
}

接口的使用拦截器:

import http from './service/index'

//登陆接口
http.request({
    url: '/login',
    method: 'POST',
    interceptor: {
        requestInterceptor: config => {
            config.timeout = 2000
            return config
        },
        //其他三种也可以写的
    }
})

//人脸识别接口
http.request({
    url: '/recognitionFace
    method: 'POST',
    interceptor: {
        requestInterceptor: config => {
            config.timeout = 60000
            return config
        },
        //其他三种也可以写的
    }
})

这里就单独对接口的超时时间进行了处理,当然还有其他的应用场景。

在上面的类中处理接口拦截器这个代码中,有个bug,就会类型会报错,因为我们一般会在统一接口中,在响应拦截的时候,就返回了res.data,这个类型我们传入过去的promise类型了,但是在resInterceptor这个接口函数中类型为:AxiosResponse,所以要想处理这个问题(就简单粗暴解决吧),改为any类型

interface customInterceptorType {
  //请求拦截(接口请求成功)
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig 
  requestInterceptorCatch?: (error: any) => any     //请求拦截(捕获接口失败)
  resInterceptor?: (res: any) => any    //响应拦截(接口响应成功)
  resInterceptorCatch?: (error: any) => any                 //响应拦截(捕获响应失败)
}

小节总结:上面的拦截情况分类三种,但是第一种和第三种,我们只需要了解,然后扩宽我们的封装思路,但是第二种的统一处理,是必须要掌握,使用起来也很简单。

3、封装loading图标

在请求接口的时候,想要用户体验感好的话,就需要给用户的提示,所以需要加上一个loading图标来提示用户,说明接口真正请求中。

这里我主要使用element-plus中的加载动画。

实现的效果:

http.request({
  url: '/get',
  method: 'GET'
}).then((res:any) => {
  console.log(res);
})

//默认情况下,就是有loading图标
http.request({
  url: '/get',
  method: 'GET',
  showLoading: false
}).then((res:any) => {
  console.log(res);
})

//设置showLoading为false, 就不展示loading图标

封装代码如下:

类型接口:

interface customRequest extends AxiosRequestConfig {
  interceptor?: customInterceptorType,   
  showLoading?: boolean   //config配置文件的类型上,添加一个showLoading属性,展示是否需要展示
}
//导入ElLoading的动画组件
import { ElLoading } from 'element-plus'
//导入ElLoading中的service返回值的类型
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'

const DEFAULT_LOADING = true   //loading 的默认值

class Http {
  protected instance: AxiosInstance
  interceptor?: customInterceptorType
  loading?: ILoadingInstance    //定义loading
  showLoading?: boolean         //是否显示loading

  constructor(config: customRequest) {
    this.instance = axios.create(config)
    this.interceptor = config?.interceptor
    //初始化默认值,如果config里面有,就使用config里面的,但是如果没有就使用默认值
    this.showLoading = config.showLoading || DEFAULT_LOADING

    this.instance.interceptors.request.use(
      (config) => {
        //loading图标加载
        if(this.showLoading) {  //当为true的时候,显示loading图标
          this.loading = ElLoading.service({
            lock: true,
            text: '正在加载中...'
          })
        }
        return config
      },
      (error) => {
        return error
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        //当响应成功的时候,关闭loading
        setTimeout(() => {
          this.loading?.close()
        }, 1000)

        return res.data
      },
      (error) => {
        //当响应失败的时候,关闭loading
        this.loading?.close()
        return error
      }
    )
  }
 
  //request方法
  request(config: customRequest): any {
    return new Promise((resolve, reject) => {
      //单独处理接口配置传递showLoading, 为false就是显示图标
      if(config.showLoading === false) {
        this.showLoading = false
      }
      //调用接口之后,无论成功还是失败,都需要把实例的showLoading改为true
      this.instance.request(config).then((res) => {
        resolve(res)
        //为什么要true, 是为了不影响下一次的接口的loading加载动画
        this.showLoading = DEFAULT_LOADING
      }).cathc((err) => {
         this.showLoading = DEFAULT_LOADING
          return err
      })
    })
  }
}

export default Http

4、暴露请求的方法

对request方法详细说明

request<T>(config: customRequest): Promise<T> {
    return new Promise((resolve) => {
        //这里的instance中的request,看源码知道,这里接收两个泛型,第二个泛型才是Promise返回的对象
        this.instance.request<any, T>(config).then((res) => {
            resolve(res)   //所以这里res的类型就为 T 了
        })
    })
}

在上面的代码中,request接收一个泛型,该方法返回一个Promise对象,这个泛型就是对象Promise的类型设定,对最后的返回执行进行设置。

对其他的方法封装:

get<T>(config: customRequest): Promise<T>{
    return this.request({...config, method: 'GET'})
}

post<T>(config: customRequest): Promise<T>{
    return this.request({...config, method: 'POST'})
}

使用封装的方法时

interface IResType {
  data: any, 
  status: number,
  success: boolean
}

http.get<IResType>({
  url: '/get'
}).then(res => {
  console.log(res);
})

在上面的代码中,data为any,但是我们想对data中的返回类型进行限定,那么该怎么处理呢?

interface IResType<T> {
  data: T,
  status: number,
  success: boolean
}

http.request<IResType<any>>({
  method: 'GET',
  url: '/get'
}).then(res => {
  console.log(res);
})

当然,要明确知道data的类型, 必须要跟后端统一,不然一方改变就会报错的,接口类型行不通

4、完整的Http类的代码封装

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

import { ElLoading } from 'element-plus'
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'

//自定义拦截器类型
interface customInterceptorType {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  resInterceptor?: (res: any) => any
  resInterceptorCatch?: (error: any) => any
}

//定义自己的实例类型
interface customRequest extends AxiosRequestConfig {
  interceptor?: customInterceptorType,
  showLoading?: boolean
}

const DEFAULT_LOADING = true   //loading 的默认值

class Http {
  protected instance: AxiosInstance
  interceptor?: customInterceptorType
  loading?: ILoadingInstance
  showLoading: boolean

  constructor(config: customRequest) {
    this.instance = axios.create(config)
    this.interceptor = config?.interceptor
    this.showLoading = config.showLoading || DEFAULT_LOADING

    //从config里面取出每个实例的不同拦截器 (可以删除,如果没有单独实例处理)
    this.instance.interceptors.request.use(
      this.interceptor?.requestInterceptor,
      this.interceptor?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptor?.resInterceptor,
      this.interceptor?.resInterceptorCatch
    )

    //统一,所以实例都会拦截
    this.instance.interceptors.request.use(
      (config) => {
        //loading图标加载
        if(this.showLoading) {
          this.loading = ElLoading.service({
            lock: true,
            text: '正在加载中...'
          })
        }
        //token拦截
        const token = localStorage.getItem('token')
        if(token) {
          config.headers.Authorization = token
        }
        return config
      },
      (error) => {
        return error
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        //当响应成功的时候,关闭loading
        setTimeout(() => {
          this.loading?.close()
        }, 1000)

        return res.data
      },
      (error) => {
        //当响应失败的时候,关闭loading
        this.loading?.close()
        return error
      }
    )
  }

  // request<T>(params: customRequest): Promise<T>
  // request<T>(...args: any[]): Promise<T>
  //对部分接口的进行拦截配置
  request<T>(params: customRequest): Promise<T> {
    let config:customRequest
    if(typeof params === 'string') {   //字符串
      // config = arguments[1] || {}
      // config.url = arguments[0]   //处理url
      // if(arguments[2]) {
      //   config.showLoading = arguments[2] || {}   //处理showLoading
      // }
    } else {  //对象
      config = params || {}
    }

    return new Promise((resolve) => {
      if (config.interceptor?.requestInterceptor) {  //可以删除,没有接口拦截器
        config = config.interceptor.requestInterceptor(config)
      }
      if(config.showLoading === false) {
        this.showLoading = false
      }
      this.instance.request<any, T>(config).then((res) => {
        if (config.interceptor?.resInterceptor) {  //可以删除,没有接口拦截器
          config = config.interceptor.resInterceptor(res)
        }
        resolve(res)
        this.showLoading = DEFAULT_LOADING
      })
    })
  }

  get<T>(config: customRequest): Promise<T>{
    return this.request({...config, method: 'GET'})
  }

  post<T>(config: customRequest): Promise<T>{
    return this.request({...config, method: 'POST'})
  }
}

export default Http

这里的代码唯一美中不足的就是,get等方法还不支持传递字符串形式,只能以对象的形式作为参数。因为目前我不知道怎么在ts使用剩余运算符,直接使用arguments,好像直接报错;第二次测试,打算用函数的重载,但是好像也是需要使用剩余参数,还没解决出来。如果以后有思路了,再来进行补充(脚步是一个一个的走的)。

5、总结

​ 在这次二次封装axios的过程中,用到了太多的ts的类型限定了,各种泛型的使用,对Promise的类型全面认识,大大的加强了我对ts的使用,对ts的安全检测类型有着全面的认识;而且这次还加强了自己的封装能力,让自己封装的函数,更加的具有扩展性,安全性,感觉这次真的收获巨大。

​ 如果其中有什么不对地方,可以提出来,虚心接受(一位菜鸟小白的成长之路)。

​ 最后想说,站在大佬(coderwhy老师)的肩膀上学习,一切不可能就能变为可能。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐