手摸手创建一个 Vue + Ts 项目(八)—— 封装 Axios 请求

850 阅读3分钟

系列目录

前言

虽然 axios 功能已经非常强大,但在实际的 axios 使用过程中,通过会针对接口来做一些通用的适配封装,这里主要在基本功能的基础上增加一些通用的方法、钩子和异常处理。

定义相关类型

在 src 文件夹中,创建 utils/http 文件夹,创建 types.ts 文件,存放封装用到的类型:

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

/**
 * 接口通用返回参数
 */
export interface ApiResponse<T> {
  code: number,
  message: string,
  data: T
}

/**
 * 请求配置属性
 */
export interface RequestOptions {
  // 是否展示 Loading 动画
  showLoading?: boolean,
  // 是否弹窗展示异常信息
  showErrorMessage?: boolean,
  // 是否进行数据转换
  transform?: boolean
}

/**
 * 拦截器钩子
 */
export interface InterceptorHooks {
  beforeRequestCallback?: (request: ExpandAxiosRequestConfig) => void
  beforeResponseCallback?: (response: ExpandAxiosResponse) => void
}

/**
 * 拓展自定义请求配置
 */
export interface ExpandAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
  interceptorHooks?: InterceptorHooks
  requestOptions?: RequestOptions
}

/**
 * 拓展 axios 请求配置
 */
export interface ExpandInternalAxiosRequestConfig<D = any> extends InternalAxiosRequestConfig<D> {
  interceptorHooks?: InterceptorHooks
  requestOptions?: RequestOptions
}

/**
 * 拓展 axios 返回配置
 */
export interface ExpandAxiosResponse<T = any, D = any> extends AxiosResponse<T, D> {
  config: ExpandInternalAxiosRequestConfig<D>
}

这里创建了几个接口类型,这里针对每个类型进行说明。

  • ApiResponse<T>:对于后端接口结构的一个封装,可以根据实际项目进行调整;
  • RequestOptions:额外添加的请求配置属性
  • InterceptorHooks:拦截器钩子,分为请求之前和响应之前两个
  • ExpandAxiosRequestConfig:axios实际请求时的一个补充,将前面封装的 RequestOptionsInterceptorHooks 添加上
  • ExpandInternalAxiosRequestConfig:axios 请求拦截器中的子类
  • ExpandAxiosResponse:axios 响应拦截器中的类型子类

封装请求对象

创建 /src/utils/http/AxiosRequest.ts 文件:

import type { AxiosInstance, AxiosResponse } from "axios";
import axios from "axios";
import {
  ApiResponse,
  ExpandAxiosRequestConfig,
  ExpandAxiosResponse,
  ExpandInternalAxiosRequestConfig,
} from "./types";

const errorStateMap = new Map([
  [400, "请求方式错误"],
  [401, "请重新登录"],
  [403, "拒绝访问"],
  [404, "请求地址不存在"],
  [500, "服务器出错"],
]);

const errorHandler = (err: any) => {
  if (err.config?.requestOptions.showLoading) {
    window.$loadingBar.error();
  }
  if (err.config?.requestOptions.showErrorMessage) {
    const message: string =
      errorStateMap.get(err.response.status) || "请求出错,请稍后重试";
    handleErrorMessage(message);
  }
  return Promise.reject(err);
};

const handleErrorMessage = (errorMessage: string) => {
  window.$message.error(errorMessage);
};

const successCode = 1;

export class AxiosRequest {
  private defaultConfig: ExpandAxiosRequestConfig = {
    // 请求超时时间
    timeout: 3000,
    requestOptions: {
      showLoading: true,
      showErrorMessage: true,
      transform: true,
    },
  };

  private axiosInstance: AxiosInstance;

  public constructor(config: ExpandAxiosRequestConfig = {}) {
    const axiosConfig = Object.assign(this.defaultConfig, config);
    this.axiosInstance = axios.create(axiosConfig);
    this.interceptRequest();
    this.interceptResponse();
  }

  /**
   * 通用请求方法
   */
  public request<D, R>(
    config: ExpandAxiosRequestConfig<D>
  ): Promise<AxiosResponse<R>> {
    return this.axiosInstance.request(config);
  }

  /**
   * get 请求
   */
  public get<D, R>(
   	url: string,
	config: ExpandAxiosRequestConfig<D> = {}
  ): Promise<R> {
    let requestOptions = config.requestOptions
    if (requestOptions) {
      requestOptions.transform = true
      config.requestOptions = requestOptions;
    }
    return this.axiosInstance.get(url, config);
  }

  /**
   * get 请求
   */
  public getNoTransRes<D, R>(
	url: string,
	config: ExpandAxiosRequestConfig<D> = {}
  ): Promise<ApiResponse<R>> {
    let requestOptions = config.requestOptions
    if (!requestOptions) {
      requestOptions = {}
    }
    requestOptions.transform = false
    config.requestOptions = requestOptions;
    return this.axiosInstance.get(url, config);
  }

  /**
   * post 请求
   */
  public post<D, R>(
	url: string,
	data?: D,
	config: ExpandAxiosRequestConfig<D> = {}
  ): Promise<R> {
	let requestOptions = config.requestOptions
	if (requestOptions) {
	  requestOptions.transform = true
	  config.requestOptions = requestOptions;
	}
	return this.axiosInstance.post(url, data, config);
  }

  /**
   * post 请求
   */
  public postNoTransRes<D, R>(
	url: string,
	data?: D,
	config: ExpandAxiosRequestConfig<D> = {}
  ): Promise<ApiResponse<R>> {
	let requestOptions = config.requestOptions
	if (!requestOptions) {
	  requestOptions = {}
	}
	requestOptions.transform = false
	config.requestOptions = requestOptions;
	return this.axiosInstance.post(url, data, config);
  }

  private interceptRequest(): void {
    this.axiosInstance.interceptors.request.use(
      async (config: ExpandInternalAxiosRequestConfig) => {
        // loadingBar
        if (config.requestOptions?.showLoading) {
          window.$loadingBar.start();
        }
        // hook
        if (config.interceptorHooks?.beforeRequestCallback) {
          config.interceptorHooks.beforeRequestCallback(config);
        }

        return config;
      },
      errorHandler
    );
  }

  private interceptResponse(): void {
    this.axiosInstance.interceptors.response.use(
      async (response: ExpandAxiosResponse): Promise<any> => {
        // loadingBar
        if (response.config.requestOptions?.showLoading) {
          window.$loadingBar.finish();
        }
        // hook
        if (response.config.interceptorHooks?.beforeResponseCallback) {
          response.config.interceptorHooks.beforeResponseCallback(response);
        }
        // transform data
        if (!response.config.requestOptions?.transform) {
          return response;
        }

        const { code, message, data } = response.data;

        if (code === successCode) {
          return data;
        } else {
          if (response.config.requestOptions?.showErrorMessage) {
            handleErrorMessage(message);
          }
        }

        return Promise.reject(response.data);
      },
      errorHandler
    );
  }
}

之后,在 src/utils/http 文件夹下创建 index.ts 文件,用于请求对象的进一步封装。

import { AxiosRequest } from "./AxiosRequest"

export default new AxiosRequest({
  baseURL: '/api'
})

使用

在使用时,可以分别定义入参和出参的类型。

例如,这里要查询用户列表:

  • 封装出入参类型
export interface UserQuery {
  pageIndex: number,
  pageSize: number
}

export interface UserInfo {
  userId: string,
  username: string,
  nickName: string,
  phoneNumber: string,
  state: string,
  loginIp?: string,
  loginDate?: string,
  remark?: string
}
  • 封装请求
import { UserQuery, UserInfo, UserAddRequest } from "./types/userTypes";
import request from '@/utils/http'

export const userPageQuery = (data: UserQuery) => {
  return request.post<UserQuery, UserInfo>('user/pageQuery', data)
};

export const userAdd = (data: UserAddRequest) => {
  return request.post<UserAddRequest, void>('user/add', data)
}