开发笔记之axios Ts版本封装

3,048 阅读4分钟

Axios的TS版本封装

最近开发的一些项目都用上了vue3.0+ts,所以针对一些全局使用的工具也进行了一些封装。Axios作为HTTP请求的工具,首当其冲。

封装思考

  1. axios的基础配置
  2. 请求拦截的封装
  3. 响应拦截的封装
  4. 取消重复请求
  5. 请求参数以及回参的泛型化--主要是完成这一步最大化享受到ts带来的类型检测以及类型推断的好处

了解axios声明文件

下面我将声明文件中在封装用到的一些摘抄出来,其实了解一个插件,最快的方法就是了解它的声明文件,提供什么方法有什么参数什么类型的参数一目了然。

  1. 首先可以看到axios支持的请求方式Method
  2. 响应数据支持的类型ResponseType
  3. 请求配置AxiosRequestConfig,其中就包括请求方式,请求类型,参数,以及一些请求头的配置
  4. 响应数据类型AxiosResponse,从这里可以了解到axios响应数据的数据结构
  5. 请求异常的数据类型AxiosError
  6. 取消请求的方法
  7. Axios支持的api
export type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'purge' | 'PURGE'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK'

export type ResponseType =
  | 'arraybuffer'
  | 'blob'
  | 'document'
  | 'json'
  | 'text'
  | 'stream'


export interface AxiosRequestConfig<T = any> {
  url?: string;
  method?: Method;
  baseURL?: string;
  transformRequest?: AxiosTransformer | AxiosTransformer[];
  transformResponse?: AxiosTransformer | AxiosTransformer[];
  headers?: Record<string, string>;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: T;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: ResponseType;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: ((status: number) => boolean) | null;
  maxBodyLength?: number;
  maxRedirects?: number;
  socketPath?: string | null;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
  decompress?: boolean;
  transitional?: TransitionalOptions
  signal?: AbortSignal;
}

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

export interface AxiosError<T = never> extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse<T>;
  isAxiosError: boolean;
  toJSON: () => object;
}

export interface Cancel {
  message: string;
}

export interface Canceler {
  (message?: string): void;
}

export interface CancelTokenStatic {
  new (executor: (cancel: Canceler) => void): CancelToken;
  source(): CancelTokenSource;
}

export interface CancelToken {
  promise: Promise<Cancel>;
  reason?: Cancel;
  throwIfRequested(): void;
}

export interface CancelTokenSource {
  token: CancelToken;
  cancel: Canceler;
}


export class Axios {
  constructor(config?: AxiosRequestConfig);
  defaults: AxiosRequestConfig;
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
  getUri(config?: AxiosRequestConfig): string;
  request<T = never, R = AxiosResponse<T>> (config: AxiosRequestConfig<T>): Promise<R>;
  get<T = never, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig<T>): Promise<R>;
  delete<T = never, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig<T>): Promise<R>;
  head<T = never, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig<T>): Promise<R>;
  options<T = never, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig<T>): Promise<R>;
  post<T = never, R = AxiosResponse<T>>(url: string, data?: T, config?: AxiosRequestConfig<T>): Promise<R>;
  put<T = never, R = AxiosResponse<T>>(url: string, data?: T, config?: AxiosRequestConfig<T>): Promise<R>;
  patch<T = never, R = AxiosResponse<T>>(url: string, data?: T, config?: AxiosRequestConfig<T>): Promise<R>;
}

export interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise;
  (url: string, config?: AxiosRequestConfig): AxiosPromise;
}

export interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance;
  Cancel: CancelStatic;
  CancelToken: CancelTokenStatic;
  Axios: typeof Axios;
  readonly VERSION: string;
  isCancel(value: any): boolean;
  all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
  spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
  isAxiosError(payload: any): payload is AxiosError;
}

declare const axios: AxiosStatic;

export default axios;

实现一个Request的类

  1. 首先先把关键性的字段声明下
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosError,
  AxiosResponse,
  CancelTokenStatic
} from 'axios'

class Request {
	instance: AxiosInstance // axios实例
	pending:Array<{
		url: string
		cancel: Function
	}> = [] // 请求url集合在每次请求之前都会查找是否存在当前接口请求,存在则取消并建立新的请求

	CancelToken: CancelTokenStatic = axios.CancelToken

	axiosRequestConfig: AxiosRequestConfig = {} // 请求配置
	successCode: Array<number> = [200,201,204] // 成功的相应状态码
	baseURL:string='/api'

	constructor() {
		// 这里会初始化axios的配置并且绑定请求以及相应的拦截配置
		this.requestConfig()
	    this.instance = axios.create(this.axiosRequestConfig)
	    this.interceptorsRequest()
	    this.interceptorsResponse()
	}
	async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<MyResponseType<T>> {}
	async post<T = any>(url: string,data?: T,config?: AxiosRequestConfig): Promise<MyResponseType<T>> {}
	requestConfig():void{}
	interceptorsRequest():void{}
	interceptorsResponse():void{}
	requestConfig():void{}
	// 取消重复请求
    removePending(config: AxiosRequestConfig): void{}
    // 请求日志
    requestLog(request: AxiosRequestConfig): void {}
    // 响应日志
    responseLog(response: AxiosResponse): void {}
}
export default new Request()

那其实完成以上的内容,大体的框架就已经完成了。接着就是把每个阶段的逻辑加入进去。

  1. axios的基础配置
  2. 请求拦截器中需要处理参数类型以及自定义请求头的设置,并且移除之前重复请求的接口并且加入当前请求接口到peding,打印请求日志
  3. 响应拦截器中针对除了successCode以外的相应状态码进行统一处理并且从peding移除当前接口请求,打印响应日志
  4. 请求方式的实现,其实就是在对应的方法里面调用axios对应的请求然后处理一下响应数据并返回

以下是完整代码:

// import http from 'http'
// import https from 'https'
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosError,
  AxiosResponse,
  CancelTokenStatic
} from 'axios'
import qs from 'qs'
import { store } from '../store'
import { MyResponseType } from './types'

export class Request {
  protected instance: AxiosInstance

  protected pending: Array<{
    url: string
    cancel: Function
  }> = []

  protected CancelToken: CancelTokenStatic = axios.CancelToken

  protected axiosRequestConfig: AxiosRequestConfig = {}

  protected successCode: Array<Number> = [200, 201, 204]

  protected baseURL: string = '/api'

  constructor() {
    this.requestConfig()
    this.instance = axios.create(this.axiosRequestConfig)
    this.interceptorsRequest()
    this.interceptorsResponse()
  }

  async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<MyResponseType<T>> {
    try {
      const data: MyResponseType<T> = await this.instance.get(url, config)
      return data
    } catch (err: any) {
      const message = err.message || '请求失败'
      return {
        code: -1,
        message,
        data: null as any
      }
    }
  }

  async post<T = any>(
    url: string,
    data?: T,
    config?: AxiosRequestConfig
  ): Promise<MyResponseType<T>> {
    try {
      const res: MyResponseType<T> = await this.instance.post(url, data, config)
      return res
    } catch (err: any) {
      const message = err.message || '请求失败'
      return {
        code: -1,
        message,
        data: null as any
      }
    }
  }

  // axios请求配置
  protected requestConfig(): void {
    this.axiosRequestConfig = {
      baseURL: this.baseURL,
      headers: {
        timestamp: String(new Date().getTime()),
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
      },
      transformRequest: [(obj) => qs.stringify(obj)],
      transformResponse: [
        (data: AxiosResponse) => {
          return data
        }
      ],
      paramsSerializer(params: any) {
        return qs.stringify(params, { arrayFormat: 'brackets' })
      },
      timeout: 30000,
      withCredentials: false,
      responseType: 'json',
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxRedirects: 5,
      maxContentLength: 2000,
      validateStatus(status: number) {
        return status >= 200 && status < 500
      }
      // httpAgent: new http.Agent({ keepAlive: true }),
      // httpsAgent: new https.Agent({ keepAlive: true })
    }
  }

  // 请求拦截
  protected interceptorsRequest() {
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        this.removePending(config)
        config.cancelToken = new this.CancelToken((c: any) => {
          this.pending.push({
            url: `${config.url}/${JSON.stringify(config.data)}&request_type=${config.method}`,
            cancel: c
          })
        })
        const token = store.getters['userModule/getToken']
        if (token) {
          Object.assign(config.headers, { 'x-token': token || '' })
        }
        this.requestLog(config)
        return config
      },
      (error: AxiosError) => {
        return Promise.reject(error)
      }
    )
  }

  // 响应拦截
  protected interceptorsResponse(): void {
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        this.responseLog(response)
        this.removePending(response.config)
        if (this.successCode.indexOf(response.status) === -1) {
          // Message({
          //   message: response.data.message || 'Error',
          //   type: 'error',
          //   duration: 5 * 1000
          // })
          // if (response.data.code === 401) {
          //   MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          //     confirmButtonText: '重新登录',
          //     cancelButtonText: '取消',
          //     type: 'warning'
          //   }).then(() => {
          //     UserModule.ResetToken()
          //     location.reload()
          //   })
          // }
          return Promise.reject(new Error(response.data || 'Error'))
        }
        return response.data
      },
      (error: AxiosError) => {
        return Promise.reject(error)
      }
    )
  }

  // 取消重复请求
  protected removePending(config: AxiosRequestConfig): void {
    this.pending.map((v, index) => {
      if (v.url === `${config.url}/${JSON.stringify(config.data)}&request_type=${config.method}`) {
        v.cancel()
        console.log('=====', this.pending)
        this.pending.splice(index, 1)
        console.log('+++++', this.pending)
      }
      return v
    })
  }

  // 请求日志
  protected requestLog(request: AxiosRequestConfig): void {
    if (process.env.NODE_ENV === 'development') {
      const randomColor = `rgba(${Math.round(Math.random() * 255)},${Math.round(
        Math.random() * 255
      )},${Math.round(Math.random() * 255)})`
      console.log(
        '%c┍------------------------------------------------------------------┑',
        `color:${randomColor};`
      )
      console.log('| 请求地址:', request.url)
      console.log(
        '| 请求参数:',
        qs.parse(
          ((request.method || 'get').toLowerCase() === 'get' ? request.params : request.data) as any
        )
      )
      console.log(
        '%c┕------------------------------------------------------------------┙',
        `color:${randomColor};`
      )
    }
  }

  // 响应日志
  protected responseLog(response: AxiosResponse): void {
    if (process.env.NODE_ENV === 'development') {
      const randomColor = `rgba(${Math.round(Math.random() * 255)},${Math.round(
        Math.random() * 255
      )},${Math.round(Math.random() * 255)})`
      console.log(
        '%c┍------------------------------------------------------------------┑',
        `color:${randomColor};`
      )
      console.log('| 请求地址:', response.config.url)
      console.log('| 请求参数:', qs.parse(response.config.data as any))
      console.log('| 返回数据:', response.data)
      console.log(
        '%c┕------------------------------------------------------------------┙',
        `color:${randomColor};`
      )
    }
  }
}

export default new Request()