Axios封装

Posted by ZSJ on April 13, 2024

Axios封装能改善原生框架的不足,能够智能感知参数类型,不需要每次调用时使用try-catch处理错误,能够支持路径参数替换。具体封装方法如下:

创建一个api目录和对应的子目录,用来保存所有与api调用相关的操作,目录结构如下:

目录结构 config目录保存各个Apiy请求的配置信息,interceptor目录保存各个拦截器,method目录保存具体的api调用方法,request目录旋转axios请求方法,types目录保存针对axios封装扩展的一些类型

在types目录下创建ApiConfig.d.ts文件, 内容如下:

interface ApiConfig {
  url: string;
  useToken: boolean;
  useBaseUrl: boolean;
}

export default ApiConfig;

在types目录下创建RequestConfig.d.ts文件,内容如下:

import type { AxiosRequestConfig } from 'axios';
import type ApiConfig from './ApiConfig';

interface RequestConfig extends AxiosRequestConfig {
  args?: Record<string, any>;
  specialConfig?: ApiConfig;
}

export default RequestConfig;

在interceptor目录下创建urlArgsReplace.ts,内容如下:

import type { InternalAxiosRequestConfig } from 'axios';
import type RequestConfig from '../types/RequestConfig';

const urlArgsReplace = {
  request: {
    onFulfilled: (config: InternalAxiosRequestConfig) => {
      const { url, args } = config as RequestConfig;
      if (args) {
        const lostParams: string[] = [];
        const replacedUrl = url!.replace(/\{([^}]+)\}/g, (_res, arg: string) => {
          if (!args[arg]) {
            lostParams.push(arg);
          }
          return args[arg] as string;
        });
        if (lostParams.length) {
          return Promise.reject(new Error('在args中找不到对应的路径参数'));
        }
        return { ...config, url: replacedUrl };
      }
      return config;
    },
  },
};

export default urlArgsReplace;

在interceptor目录下创建文件specialRequestConfig.ts, 内容如下:

import type { InternalAxiosRequestConfig } from 'axios';
import type RequestConfig from '../types/RequestConfig';

const specialRequestConfig = {
  request: {
    onFulfilled: async (config: InternalAxiosRequestConfig) => {
      console.log(`specialRequestConfig all config: ${JSON.stringify(config)}`);
      const { specialConfig } = config as RequestConfig;
      console.log(`specialRequestConfig specialConfig config: ${JSON.stringify(specialConfig)}`);
      if (specialConfig) {
        if (specialConfig.useBaseUrl) {
          // window.webConfig.apiUrl是一个全局变量,保存了api baseURL, 可以改成自己合适的方式
          config.baseURL = window.webConfig.apiUrl;
        }
        if (specialConfig.useToken) {
          // 从localStorage中获取token, 需要改成合适的方式
          const data = localStorage.getItem('ZSJTESTWEBTOKEN');
          config.headers.Authorization = `Bearer ${data}`;
        }
        if (!config.url && specialConfig.url) {
          config.url = specialConfig.url;
        }
        return { ...config };
      }
      return config;
    },
  },
};

export default specialRequestConfig;

在request目录下创建index.ts文件,内容如下:

import axios from 'axios';
import type { AxiosError, AxiosResponse } from 'axios';
import urlArgsReplace from '../interceptor/urlArgsReplace';
import specialRequestConfig from '../interceptor/specialRequestConfig';
import type ApiConfig from '../types/ApiConfig';
import type RequestConfig from '../types/RequestConfig';

const instance = axios.create({
  timeout: 240000,
  baseURL: '',
});

instance.interceptors.request.use(specialRequestConfig.request.onFulfilled, undefined);
instance.interceptors.request.use(urlArgsReplace.request.onFulfilled, undefined);

export interface ResultFormat<T = any> {
  data: null | T;
  error: AxiosError | null;
  response: AxiosResponse<T> | null;
}

/**
* 允许定义六个可选的泛型参数:
*    Payload: 用于定义响应结果的数据类型
*    Data:用于定义data的数据类型
*    Params:用于定义parmas的数据类型
*    Args:用于定义存放路径参数的属性args的数据类型
*    Headers: 用于定义额外的Header信息 (可以直接写在调用方法体内, 也可以作为泛型参数传入)
*    SpecialConifg: 用于定义额外的SpecialConfig信息(可以直接写在调用方法体内, 也可以作为泛型参数传入)
*/
interface MakeRequest {
  <Payload = any>(config: RequestConfig): (
    requestConfig?: Partial<RequestConfig>,
  ) => Promise<ResultFormat<Payload>>;

  <Payload, Data>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data'>> & { data: Data },
  ) => Promise<ResultFormat<Payload>>;

  <Payload, Data, Params>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) & { params: Params },
  ) => Promise<ResultFormat<Payload>>;

  <Payload, Data, Params, Args>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params' | 'args'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) &
      (Params extends undefined ? { params?: undefined } : { params: Params }) & {
        args: Args;
      },
  ) => Promise<ResultFormat<Payload>>;

  <Payload, Data, Params, Args, Headers>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params' | 'args' | 'headers'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) &
      (Params extends undefined ? { params?: undefined } : { params: Params }) &
      (Headers extends undefined ? { headers?: undefined } : { headers: Headers }) & {
        args: Args;
      },
  ) => Promise<ResultFormat<Payload>>;

  <Payload, Data, Params, Args, Headers>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params' | 'args' | 'headers' | 'specialConfig'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) &
      (Params extends undefined ? { params?: undefined } : { params: Params }) &
      (Headers extends undefined ? { headers?: undefined } : { headers: Headers }) & {
        args: Args;
        specialConfig: ApiConfig;
      },
  ) => Promise<ResultFormat<Payload>>;
}

// eslint-disable-next-line arrow-body-style
const makeRequest: MakeRequest = <T>(config: RequestConfig) => {
  return async (requestConfig?: Partial<RequestConfig>) => {
    // 合并在service中定义的option和调用时从外部传入的option
    const mergedConfig: RequestConfig = {
      ...config,
      ...requestConfig,
      headers: {
        ...config.headers,
        ...requestConfig?.headers,
      },
    };
    // 统一处理返回类型
    try {
      const response: AxiosResponse<T, RequestConfig> = await instance.request<T>(mergedConfig);
      const { data } = response;
      console.log(`response: ${JSON.stringify(response)}`);
      return { error: null, data, response };
    } catch (error: any) {
      return { error, data: null, response: null };
    }
  };
};

export default makeRequest;

在config目录下创建requestApiConfig.ts文件,其内容如下:

import type ApiConfig from "../types/ApiConfig";

// 测试使用的api,需要修改成正式的api 
const enum ApiName {
  userLogin = 'userLogin',
  etickets = 'etickets',
  eticketstats = 'eticketstats',
  testapi = 'testapi',
}

type ApiNameStrings = keyof typeof ApiName;

const requestApiConfig : Record<ApiNameStrings, ApiConfig> = {
  userLogin: {
    url: 'userLogins',
    useToken: false,
    useBaseUrl: true,
  },
  etickets: {
    url: 'etickets',
    useToken: true,
    useBaseUrl: true,
  },
  eticketstats: {
    url: 'etickets/stats',
    useToken: true,
    useBaseUrl: true,
  },
  testapi: {
    url: 'values',
    useToken: false,
    useBaseUrl: true,
  }
};
export type { ApiName, ApiNameStrings };
export default requestApiConfig;

在method目录下添加get.ts, 保存所有的get调用,其内容如下,如有其他调用,在文件内继续添加:

import makeRequest from '../request';
import requestApiConfig from '../config/requestApiConfig';
import type ETicket from '../../types/ETicket';

const method = 'get';

/* api名称手工提供,以便可以创建不在apiConfig中的记录, 或者同一api有从种get方式 */
export default {
  etickets: makeRequest<ETicket[],
    undefined,
    {
      createdDateFrom: Date; createDateTimeTo: Date; pageIndex: number; pageSize: number;
    }>({
      method,
      specialConfig: requestApiConfig.etickets,
    }),
  eticketsDetail: makeRequest<
    ETicket,
    undefined,
    undefined,
    { id: number }
  >({
    url: `${requestApiConfig.etickets.url}/{id}`,
    specialConfig: requestApiConfig.etickets,
  }),
  eticketsStats: makeRequest<
    number,
    undefined,
    {
      createdDateFrom: Date; createDateTimeTo: Date; pageIndex: number; pageSize: number;
    }
  >({
    method,
    specialConfig: requestApiConfig.eticketstats,
  }),
  testapi: makeRequest<Array<string>
  >({
    method,
    specialConfig: requestApiConfig.testapi,
  }),
};

在method目录下添加post.ts, 保存所有的post调用,其内容如下,如有其他调用,在文件内继续添加:

import makeRequest from '../request';
import TokenResponse from '../../types/TokenResponse';

const method = 'post';

/* api名称手工提供,以便可以创建不在apiConfig中的记录 */

export default {
  userLogin: makeRequest<TokenResponse,
    { username: string; password: string }>({
      method,
    }),
};

如果有put, delete, patch等api调用,在method下继续添加即可

在api目录下添加index.ts,将method下的所有方法统一导出

import post from './method/post';
import get from './method/get';

const api = {
  get,
  post,
};

export default api;

使用示例:

get调用 post调用