likes
comments
collection
share

基于axios封装的方法工厂

作者站长头像
站长
· 阅读数 79

我们在做项目的时候很多时候会在Api中定义很多请求的方法,导致我们会写很多的函数以及重复性的代码。为了解决这个问题,我基于axios封装了一个方法工厂。

使用方法工厂不需要在Api中再去写入很多的函数,只需要在对应的文件中配置好请求参数等信息,就可以根据所配置的请求信息动态的创建出所需要的方法随用随创建即可。

最后方法工厂所能达到的目的:

  1. 拦截器目的更具性处理
  2. 根据配置动态创建方法
  3. 可以支持默认参数
  4. 参数支持query拼接,body请求体,params地址动态参数

为了遵循设计模式单一职责需要创建两个类分别是AxiosFactoryRequest,其中AxiosFactory主要作用用来动态创建方法,Request用来处理axios相关内容。

class Request { 
    //...
};
class AxiosFactory extends Request {
    //...
};

首先处理的是拦截器部分,在项目中拦截器在使用的时候不太方便使用,虽然在使用过程中已经区分了请求拦截和响应拦截,但是混在一起有的时候确实不太好区分,有的时候还有可能会搞混了,axios拦截器使用如下代码所示:

import axios from "axios";
import type {
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError
} from "axios";

const request = axios.create({
    baseUrl: "https://juejin.cn/"
});
// 请求拦截
request.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig => {
    // 处理逻辑
    return config;
}, (error: AxiosError): AxiosError => {
    // 处理逻辑
    return error;
});
// 响应拦截
axiosInstance.interceptors.response.use((response: AxiosResponse): AxiosResponse => {
    // 处理逻辑
    return response;
}, (error: AxiosError): AxiosError => {
    // 处理逻辑
    return error;
});

这里对拦截器进行简易的封装,让拦截器看起来更具有针对性:

import axios from "axios";
import type {
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError
} from "axios";
import type {
  Intercept
} from "types/Request";
// 接口应该放在 **.d.ts 中
interface Intercept { 
  requestIntercept?: (config: AxiosRequestConfig) => AxiosRequestConfig;
  requestError?: (error: AxiosError) => AxiosError;
  responseIntercept?: (config: AxiosResponse) => AxiosResponse;
  responseError?: (error: AxiosError) => AxiosError;
}

class Request { 
  private readonly axiosInstance;

  constructor(config: AxiosRequestConfig) { 
    //axios实例
    this.axiosInstance = axios.create(config);
  };

  interceptors(intercept: Intercept): void { 
    const { axiosInstance } = this;
    // 请求拦截函数
    const requestIntercept = intercept.requestIntercept || ((config: AxiosRequestConfig) => config);
    // 请求错误函数
    const requestError = intercept.requestError || ((error: AxiosError): AxiosError => error);
    // 响应拦截函数
    const responseIntercept = intercept.responseIntercept || ((config: AxiosResponse) => config);
    // 响应错误函数
    const responseError = intercept.responseError || ((error: AxiosError): AxiosError => error);
    // 请求拦截器
    axiosInstance.interceptors.request.use(requestIntercept, requestError);
    // 响应拦截器
    axiosInstance.interceptors.response.use(responseIntercept, responseError);
  };
}

Request类中使用interceptors进行二次封装处理,使用对象对不同的拦截器的使用函数收,为了保证拦截器的正常工作,分别为其提供了不同的默认值。

在使用的时候只需要使用对象传入对应的方法即可,其实这里可以看出对弈拦截器的处理并没有做太多的工作,这样处理只是想让拦截器更加的清晰而已。

import Request from "utils/AxiosFactory/Request";
import type {
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError
} from "axios";
// 创建axios实例
const request = new Request({
  baseURL: "https://juejin.cn/"
});
// 请求拦截函数
const requestIntercept = (config: AxiosRequestConfig): AxiosRequestConfig => {
  console.log("请求拦截");
  return config;
};
// 响应拦截函数
const responseIntercept = (response: AxiosResponse): AxiosResponse => {
  console.log("响应拦截");
  return response;
};
// 处理错误函数
const processingError = (error: AxiosError): AxiosError => {
  console.log("出错了");
  return error;
};
// 使用拦截器
request.interceptors({
  requestIntercept,
  requestError: processingError,
  responseIntercept,
  responseError: processingError
});

完成了对拦截器的处理,接下来就需要完成剩下的需求,支持可以配置化请求信息,这里的程序设想是这样的,需要把所需要的配置统一存储到类中,之后再通过方法动态创建出所需要的请求函数,还有一个比较重要的一点就是,一般项目中axios实例只会用到一个,很少情况会出现一个项目中会出现多个axios

定义了AxiosFactory工厂方法,为了保证axios实例有且只有一个,需要给AxiosFactory使用单例模式保证其只有一个实例,并在类中定义一个RequestConfigMap用来存储收集到的配置的请求信息。

import Request from "./Request";
import type { AxiosRequestConfig } from "axios";
import type {
  RequestItem
} from "types/Request";

class AxiosFactory extends Request {
  // 存储收集到的请求信息
  private RequestConfigMap:any = {};
  // 实例
  static instance:AxiosFactory;

  constructor(config: AxiosRequestConfig) { 
    // 继承用于实例化axios
    super(config);
    // 实现单例
    if (AxiosFactory.instance === undefined) {
      AxiosFactory.instance = this;
    }
    return AxiosFactory.instance;
  };
};

AxiosFactory完成了基础工作,接下来需要做的就是收集依赖和创建对应的请求方法并返回。

// 接口应该放在 **.d.ts 中
interface RequestItem extends AxiosRequestConfig { 
  key?: string; // 调用方法key
  url?: string; // 请求地址
  method?: Method; // 请求方式
  data?: any; // body 请求体
  query?: any; // query请求方式 ?拼接
  params?: any;  // 路由动态参数 url/**/**
  onlyIn?: boolean; // 特有参数,是否只使用内部参数(后面会单独说明参数作用)
};

class AxiosFactory extends Request {
  // ... 其他代码
    
  // 依赖收集  
  public setConfig(name: string, requestConfigs: RequestItem[]): void { 
    // 把收集到请求信息存储起来
    this.RequestConfigMap[name] = requestConfigs;
  };
  
  // 根据收集到的依赖动态创建方法
  public create(name: string): any {
    // 收集到的请求信息
    const { RequestConfigMap } = this;
    // 读取存储信息
    const requestConfigs = RequestConfigMap[name];
    // 存储创建方法
    let methodsMap: any = {};
    // 如果读取的请求信息不存在
    if (!RequestConfigMap) {
      // 提示错误
      console.error("未找到对应集合,请使用新建setConfig添加");
      return methodsMap;
    };
    // 遍历读取到的请求信息
    for (let i = 0; i < requestConfigs.length; i++) { 
      // 请求信息每一项
      const requestItem: RequestItem = requestConfigs[i];
      // 读取key 用于最终调用方法
      const { key = "" } = requestItem;
      // 完成创建逻辑(后面会说明)
      const fn = this.createMethod(requestItem);
      // 添加所创建的方法
      methodsMap[key] = fn;
    }
    // 返回所有方法
    return methodsMap;
  };
}

通过上述方法,完成了对请求信息的收集以及动态创建并且返回对应的方法,然而并没有实现如何动态创建方法,接下来需要完成这部分的代码,这部分代码是和axios相关,所以这部分代码写入到了Request类中。

class Request {

  // ... 其他代码

  // 创建请求方法
  // RequestItem 类型上面有介绍
  public createMethod(requestItem: RequestItem): Fun { 
    // 获取axios实例
    const { axiosInstance } = this;
    // 请求信息
    const {
      // url地址
      url: beforeUrl = "",
      // 请求方式
      method = "get",
      // 默认请求体参数
      data: outData = {},
      // 默认query参数
      query: outQuery = {},
      // 默认params参数
      params: outParams = {},
      // 特有参数
      onlyIn: outOnlyIn = false
    } = requestItem;
    const that = this;
    // 返回方法
    return function (internalConfig: RequestItem = {}): any { 
      const { 
        // 内部请求体参数
        data: internalData = {},
        // 内部query参数
        query: internalQuery = {},
        // 内部params参数
        params: internalParams = {},
        // 特有参数
        onlyIn: internalOnlyIn = false
      } = internalConfig;
      // 是否内部
      const isIn = internalOnlyIn || outOnlyIn;
      // 请求信息
      const reuqestConfig: any = {};
      // 如果只使用内部
      if (!isIn) {
        // 合并请求体参数
        const afterData = that.margeData(outData, internalData);
        reuqestConfig.data = afterData;
        // 合并query参数
        const afterQuery = that.margeData(outQuery, internalQuery);
        reuqestConfig.params = afterQuery;
      } else { 
        // 直接使用内部参数
        reuqestConfig.data = internalData;
        reuqestConfig.params = internalQuery;
      };
      // 如果是内部使用内部params参数
      const beforeParams = isIn ? internalParams : that.margeData(outParams, internalParams);
      // 处理动态参数
      const url = that.withParam(beforeUrl, beforeParams);
      // 请求方法
      return axiosInstance({
        url,
        method,
        ...reuqestConfig
      })
    }
  }
  
  // 合并数据 内部参数优先于外部参数
  // 如果key相同替换掉外部参数
  private margeData(outData: any, innerData: any ): any { 
    return Object.assign({}, outData, innerData);
  };
  
  // 处理params参数
  private withParam(url: string, params: any): string { 
    // 获取所有key
    let keys = Object.keys(params);
    // 如果没有值直接返回原有 url 地址
    if (!keys.length) return url;
    // 替换特殊标识符
    // url/{id}/{age} 
    // params对象 {id:1,age:18}
    // 处理后 url/1/18
    return url.replace(/\{(\w+)\}/gi, ($1, $2) => {
      return params[$2] || "";
    });
  };
}

上述配置中一直提到的onlyIn这个配置参数,有的时候可能在请求的时候可能使用FormData对象,但是一旦使用对象对象合并处理之后FormData就会失效,所以我们需要只使用调用方法时所传入的参数,onlyIn即代表是否只使用内部参数。

完成上述代码就完成了所有功能,和最初的需求相对比就已经完成了所有的功能,接下来就需要实践一下所封装方法工厂是否可用。

实例化工厂函数:

// api/index.ts
import Request from "utils/AxiosFactory/index";
import type {
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError
} from "axios";
const request = new Request({
  baseURL: "https://autumnfish.cn/"
});
const requestIntercept = (config: AxiosRequestConfig): AxiosRequestConfig => {
  console.log("请求拦截");
  return config;
};
const responseIntercept = (response: AxiosResponse): AxiosResponse => {
  console.log("响应拦截");
  return response;
};
const processingError = (error: AxiosError): AxiosError => {
  console.log("出错了");
  return error;
};
request.interceptors({
  requestIntercept,
  requestError: processingError,
  responseIntercept,
  responseError: processingError
});
export default request;

使用:

<!-- Vue3.0 -->
<template>
  <div>
    <ul class="m-[10px]">
      <li class="border-solid divide-y border-[1px] rounded-[4px] text-center"
          v-for="(song) of state.songs"
          :key="song.id">{{ song.name }}</li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
import axios from "api/index";
import { reactive, onMounted } from "vue";
const state: {
  songs: any[]
} = reactive({
  songs: []
});

axios.setConfig("test", [{
  key: "getAbc",
  url: "/search",
  method: "get",
  query: {
    abc: 1
  }
}]);
const methods = axios.create("test");

onMounted(async () => { 
  const { data } = await methods.getAbc({
    query: { keywords: "北京" }
  });
  const { result } = data;
  state.songs = result.songs || [{ name: "北京" }];
})
</script>

我们的配置收集可以单独的放到一个文件中,在文件中统计进行收集,如果不想重复做类似的操作的的话可以使用文件扫描,一次性收集到所有的请求信息。我们只需要在调用的时候使用创建所需要的方法即可。

本次封装虽然仍会存在一些问题,比如上传文件,特定方法axios设置header等。。。如果想实现这些也是可以的需要对代码进行微调整即可。

最后附上GitHub地址:方法工厂

写在最后:

前端的路很漫长,都在成长中,所做的每一件事情可能不是最好的,但是提出想法就可以。大家可以在评论区讨论,有什么建议我会及时修改仓库和文章代码。

转载自:https://juejin.cn/post/7174735610362953788
评论
请登录