为什么要写这篇文章

本文正在参加「金石计划 . 分割6万现金大奖」

事前提示:阅览本文存在不同主意时,能够在谈论中表达,但请勿运用过激的措辞。

现在上已经有许多关于axios封装的文章。自己在初次阅览这些文章中,见识到许多封装思路,但在付诸实践时一向有疑问:这些看似高档的二次封装,是否会把axios的调用办法弄得愈加复杂? 优秀的二次封装,有以下特点:

  1. 能改善原生结构上的缺乏:明确原生结构的缺陷,且在二次封装后能完全根绝这些缺陷,与此同时不会引进新的缺陷。
  2. 保持原有的功用:当进行二次封装时,新结构的 API 可能会更改原生结构的 API 的调用办法(例如传参办法),但咱们要确保能经过新 API 调用原生 API 上的一切功用。
  3. 了解成本低:有原生结构运用经验的开发者在面临二次封装的结构和 API 时能敏捷了解且上手。

但现在我见过,或许我接收过的项目里众多的axios二次封装中,并不具有上述原则,咱们接下盘点一些常见的初级的二次封装的手法。

盘点那些初级的axios二次封装办法

1. 对特定 method 封装成新的 API,却露出很少的参数

例如以下代码:

export const post = (url, data, params) => {
  return new Promise((resolve) => {
    axios
      .post(url, data, { params })
      .then((result) => {
        resolve([null, result.data]);
      })
      .catch((err) => {
        resolve([err, undefined]);
      });
  });
};

上面的代码中对methodpost的恳求办法进行封装,用于处理原生 API 中在处理报错时需求用try~catch包裹。但这种封装有一个缺陷:整个post办法只露出了url,data,params三个参数,一般这三个参数能够满足大多数简略恳求。可是,假如咱们遇到一个特别的post接口,它的呼应时刻较慢,需求设置较长的超时时刻,那上面的post办法就立马嗝屁了。

此刻用原生的axios.post办法能够轻松搞定上述特别场景,如下所示:

// 针对此次恳求把超时时刻设置为15s
axios.post("/submit", form, { timeout: 15000 });

类似的特别场景还有许多,例如:

  1. 需求上传表单,表单中不只含数据还有文件,那只能设置headers["Content-Type"]"multipart/form-data"进行恳求,假如要显现上传文件的进度条,则还要设置onUploadProgress特点。
  2. 存在需求防止数据竞态的接口,那只能设置cancelTokensignal。有人说能够在经过拦截器interceptors一致处理以防止竞态并发,对此我举个用以对立的场景:假如同一个页面中有两个或多个下拉框,两个下拉框都会调用同一个接口获取下拉选项,那你这个用拦截器完成的防止数据竞态的机制就会呈现问题,由于会导致这些下拉框中只要一个恳求不会被中止。

有些开发者会说不会呈现这种接口,已经约定好的一切post接口只需这三种参数就行。对此我想辩驳:一个有潜力的项目总会不断地参加更多的需求,假如你觉得你的项目是没有潜力的,那当我没说。但假如你不敢肯定你的项目之后是否会参加更多特性,不敢确保是否会遇到这类特别场景,那请你在二次封装时,尽可能地保持与原生API对齐,以确保原生API中一切能做到的,二次封装后的新API也能做到。以防止在遇到上述的特别状况时,你只能尴尬地修正新API,并且还会呈现为了兼容因而改得特别难看那种写法。

2. 封装创立axios实例的办法,或许封装自界说axios

例如以下代码:

// 1. 封装创立`axios`实例的办法
const createAxiosByinterceptors = (config) => {
  const instance = axios.create({
    timeout: 1000,
    withCredentials: true,
    ...config,
  });
  instance.interceptors.request.use(xxx, xxx);
  instance.interceptors.response.use(xxx, xxx);
  return instance;
};
// 2. 封装自界说`axios`类
class Request {
  instance: AxiosInstance
  interceptorsObj?: RequestInterceptors
  constructor(config: RequestConfig) {
    this.instance = axios.create(config)
    this.interceptorsObj = config.interceptors
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch,
    )
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch,
    )
  }
}

上面的两种写法都是用于创立多个不同装备和不同拦截器的axios实例以应付多个场景。对此我想标明自己的观点:一个前端项目中,只能存在一个axios实例。多个axios实例会增加代码了解成本,让参与或许接手项目的开发者花更多的时刻去考虑和接受每个axios实例的用处和场景,就比如一个项目多个VuexRedux相同鸡肋。

那么有开发者会问假如有相当数量的接口需求用到不同的装备和拦截器,那要怎么办?下面我来分多个装备多个拦截器两种场景进行分析:

1. 多个装备下的处理办法

假如有两种或以上不同的装备,这些装备各被一部分接口运用。那么就应该声明对应不同装备的常量,然后在调用axios时传入对应的装备常量,如下所示:

// 声明含不同装备项的常量configA和configB
const configA = {
  // ....
};
const configB = {
  // ....
};
// 在需求这些装备的接口里把对应的常量传进去
axios.get("api1", configA);
axios.get("api2", configB);

对比起多个不同装备的axios实例,上述的写法愈加直观,能让阅览代码的人直接看出差异。

2. 多个拦截器下的处理办法

假如有两种或以上不同的拦截器,这些拦截器中各被一部分接口运用。那么,咱们能够把这些拦截器都挂载到全局仅有的axios实例上,然后经过以下两种办法来让拦截器选择性履行:

  1. 引荐:在config中新加一个自界说特点以决议拦截器是否履行,代码如下所示:

    调用恳求时,写法如下所示:

    instance.get("/api", {
      //新增自界说参数enableIcp来决议是否履行拦截器
      enableIcp: true,
    });
    

    在拦截器中,咱们这么编写逻辑

    // 恳求拦截器写法
    instance.interceptors.request.use(
      // onFulfilled写法
      (config: RequestConfig) => {
        // 从config取出enableIcp
        const { enableIcp } = config;
        if (enableIcp) {
          //...履行逻辑
        }
        return config;
      },
      // onRejected写法
      (error) => {
        // 从error中取出config装备
        const { config } = error;
        // 从config取出enableIcp
        const { enableIcp } = config;
        if (enableIcp) {
          //...履行逻辑
        }
        return error;
      }
    );
    // 呼应拦截器写法
    instance.interceptors.response.use(
      // onFulfilled写法
      (response) => {
        // 从response中取出config装备
        const { config } = response;
        // 从config取出enableIcp
        const { enableIcp } = config;
        if (enableIcp) {
          //...履行逻辑
        }
        return response;
      },
      // onRejected写法
      (error) => {
        // 从error中取出config装备
        const { config } = error;
        // 从config取出enableIcp
        const { enableIcp } = config;
        if (enableIcp) {
          //...履行逻辑
        }
        return error;
      }
    );
    

    经过以上写法,咱们就能够经过config.enableIcp来决议所注册拦截器的拦截器是否履行。触类旁通来说,咱们能够经过往config塞自界说特点,同时在编写拦截器时合作,就能够完美的控制单个或多个拦截器的履行与否。

  2. 次要引荐:运用axios官方供给的runWhen特点来决议拦截器是否履行,留意该特点只能决议恳求拦截器的履行与否,不能决议呼应拦截器的履行与否。用法如下所示:

    function onGetCall(config) {
      return config.method === "get";
    }
    axios.interceptors.request.use(
      function (config) {
        config.headers.test = "special get headers";
        return config;
      },
      null,
      // onGetCall的履行成果为false时,表示不履行该拦截器
      { runWhen: onGetCall }
    );
    

    关于runWhen更多用法可看axios#interceptors

本章总结

当咱们进行二次封装时,切勿为了封装而封装,首先要分析原有结构的缺陷,下面咱们来分析一下axios现在有什么缺陷。

盘点axios现在的缺陷

1. 不能智能推导params

axios的类型文件中,config变量对应的类型AxiosRequestConfig如下所示:

export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method | string;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: AxiosRequestHeaders;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: D;
  timeout?: number;
  // ...其他特点省略
}

可看出咱们能够经过泛型界说data的类型,但params被写死成any类型因此无法界说。

2. 处理过错时需求用try~catch

这个应该是许多axios二次封装都会处理的问题。当恳求报错时,axios会直接抛出过错。需求开发者用try~catch包裹着,如下所示:

try {
  const response = await axios("/get");
} catch (err) {
  // ...处理过错的逻辑
}

假如每次都要用try~catch代码块去包裹调用接口的代码行,会很繁琐。

3. 不支撑途径参数替换

现在大多数后端露出的接口格局都遵从RESTful风格,而RESTful风格的url中需求把参数值嵌到途径中,例如存在RESTful风格的接口,url/api/member/{member_id},用于获取成员的信息,调用中咱们需求把member_id替换成实践值,如下所示:

axios(`/api/member/${member_id}`);

假如把其封装成一个恳求资源办法,就要额外露出对应途径参数的形参。十分不美观,如下所示:

function getMember(member_id, config) {
  axios(`/api/member/${member_id}`, config);
}

针对上述缺陷,下面我共享一下自己精简的二次封装。

应该怎么精简地进行二次封装

在本节中我会结合Typescript来展现怎么精简地进行二次封装以处理上述axios的缺陷。留意在这次封装中我不会写任何涉及到业务上的场景例如鉴权登录过错码映射。下面先展现一下二次封装后的运用办法。

本次二次封装后的一切代码可在enhance-axios-frame中查看。

运用办法以及作用

运用办法如下所示:

apis[method][url](config);

method对应接口的恳求办法;url为接口途径;config则是AxiosConfig,也便是装备。

回来成果的数据类型为:

{
  // 当恳求报错时,data为null值
  data: null | T;
  // 当恳求报错时,err为AxiosError类型的过错实例
  err: AxiosError | null;
  // 当恳求报错时,response为null值
  response: AxiosResponse<T> | null;
}

下面来展现一下运用作用:

  1. 支撑url智能推导,且依据输入的url推导出需求的paramsdata。在缺写或写错恳求参数时,会呈现ts过错提示

    举两个接口做比如:

    • 途径为/register,办法为postdata数据类型为{ username: string; password: string }
    • 途径为/password,办法为putdata数据类型为{ password: string }params数据类型为{ username: string }

    调用作用如下所示:

    一篇拒绝初级封装axios的文章

    一篇拒绝初级封装axios的文章

    经过这种办法,咱们无需再经过一个函数来履行恳求接口逻辑,而是能够直接经过调用api来履行恳求接口逻辑。如下所示:

    // ------------曾经的办法-------------
    // 需求用一个registerAccount函数来包裹着恳求代码行
    function register(
      data: { username: string; password: string },
      config: AxiosConfig
    ) {
      return instance.post("/register", data, config);
    }
    const App = () => {
      const registerAccount = async (username, password) => {
        const response = await register({ username, password });
        //... 呼应完毕后处理逻辑
      };
      return <button onClick={registerAccount}>注册账号</button>;
    };
    // ------------现在的办法-------------
    const App = () => {
      const registerAccount = async (username, password) => {
        // 直接调用apis
        const response = await apis.post["/register"]({ username, password });
        //... 呼应完毕后处理逻辑
      };
      return <button onClick={registerAccount}>注册账号</button>;
    };
    

    以往咱们假如想在组件里调用一个已写在前端代码里的接口,则需求先知道接口的url(如上面的/register),再去经过url在前端代码里找到该接口对应的恳求函数(如上面的register)。而假如用本文这种做法,咱们只需求知道url就能够。

    这么做还有一个好处是防止重复记录接口。

  2. 支撑回来成果的智能推导

    举一个接口为比如:

    • 途径为/admin,办法为get,回来成果的数据类型为{admins: string[]}

    调用作用如下所示:

    一篇拒绝初级封装axios的文章

  3. 支撑过错捕捉,无需写try~catch包裹处理

    调用时写法如下所示:

    const getAdmins = async () => {
     const { err, data } = await apis.get['/admins']();
     // 判别假如err不为空,则代表恳求出错
     if (err) {
       //.. 处理过错的逻辑
       // 终究return跳出,防止履行下面的逻辑
       return
     };
     // 假如err为空,代表恳求正常,此刻需求用!强制声明data不为null
     setAdmins(data!.admins);
    };
    
  4. 支撑途径参数,且途径参数也是会智能推导的

    举一个接口为比如:

    • 途径为/account/{username},办法为get,需求username途径参数

    写法如下所示:

    const getAccount = async () => {
      const { err, data } = await apis.get["/account/{username}"]({
        // config新增args特点,且在里面界说username的值。终究url会被替换为/account/123
        args: {
          username: "123",
        },
      });
      if (err) return;
      setAccount(data);
    };
    

完成办法

先展现二次封装后的 API 层目录

一篇拒绝初级封装axios的文章

咱们先看/apis.index.ts的代码

import deleteApis from "./apis/delete";
import get from "./apis/get";
import post from "./apis/post";
import put from "./apis/put";
// 每一个特点中会包括同名的恳求办法下一切接口的恳求函数
const apis = {
  get,
  post,
  put,
  delete: deleteApis,
};
export default apis;

逻辑上很简略,只担任导出包括一切恳求的apis对象。接下来看看/apis/get.ts

import makeRequest from "../request";
export default {
  "/admins": makeRequest<{ admins: string[] }>({
    url: "/admins",
  }),
  "/delay": makeRequest({
    url: "/delay",
  }),
  "/500-error": makeRequest({
    url: "/500-error",
  }),
  // makeRequest用于生成支撑智能推导,途径替换,捕获过错的恳求函数
  // 其形参的类型为RequestConfig,该类型在承继AxiosConfig上加了些自界说特点,例如寄存途径参数的特点args
  // makeRequest带有四个可选泛型,分别为:
  //  - Payload: 用于界说呼应成果的数据类型,若没有则可界说为undefined,下面的变量也相同
  //  - Data:用于界说data的数据类型
  //  - Params:用于界说parmas的数据类型
  //  - Args:用于界说寄存途径参数的特点args的数据类型
  "/account/{username}": makeRequest<
    { id: string; name: string; role: string },
    undefined,
    undefined,
    { username: string }
  >({
    url: "/account/{username}",
  }),
};

一切的重点在于makeRequest,其作用我再注释里已经说了,就不再重复了。值得一提的是,咱们在调用apis.get['xx'](config1)中的config1是装备,这儿生成恳求函数的makeRequest(config2)config2也是装备,这两个装备在终究会合并在一同。这么设计的好处便是,假如有一个接口需求特别装备,例如需求更长的timeout,能够直接在makeRequest这儿就加上timeout特点如下所示:

{
  // 这是一个耗时较长的接口
  '/longtime':  makeRequest({
    url: '/longtime',
    // 设置超时时刻
    timeout: 15000
  }),
}

这样咱们每次在开发中调用apis.get['/longtime']时就不需求再界说timeout了。

额外说一种状况,假如恳求里的body需求放入FormData类型的表单数据,则能够用下面的状况处理:

export default {
  "/register": makeRequest<null, { username: string; password: string }>({
    url: "/register",
    method,
    // 把Content-Type设为multipart/form-data后,axios内部会主动把{ username: string; password: string }对象转换为待同特点的FormData类型的变量
    headers: {
      "Content-Type": "multipart/form-data",
    },
  }),
};

关于上述详情可看axios#-automatic-serialization-to-formdata。

下面来看看界说makeRequest办法的/api/request/index.ts文件:

import urlArgs from "./interceptor/url-args";
const instance = axios.create({
  timeout: 10000,
  baseURL: "/api",
});
// 经过拦截器完成途径参数替换机制,之后会放出urlArgs代码
instance.interceptors.request.use(urlArgs.request.onFulfilled, undefined);
// 界说回来成果的数据类型
export interface ResultFormat<T = any> {
  data: null | T;
  err: AxiosError | null;
  response: AxiosResponse<T> | null;
}
// 重新界说RequestConfig,在AxiosRequestConfig基础上再加args数据
export interface RequestConfig extends AxiosRequestConfig {
  args?: Record<string, any>;
}
/**
 * 允许界说四个可选的泛型参数:
 *    Payload: 用于界说呼应成果的数据类型
 *    Data:用于界说data的数据类型
 *    Params:用于界说parmas的数据类型
 *    Args:用于界说寄存途径参数的特点args的数据类型
 */
// 这儿的界说中重点处理上述四个泛型在缺省和界说下的四种不同状况
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>>;
}
const makeRequest: MakeRequest = <T>(config: RequestConfig) => {
  return async (requestConfig?: Partial<RequestConfig>) => {
    // 合并在service中界说的config和调用时从外部传入的config
    const mergedConfig: RequestConfig = {
      ...config,
      ...requestConfig,
      headers: {
        ...config.headers,
        ...requestConfig?.headers,
      },
    };
    // 一致处理回来类型
    try {
      const response: AxiosResponse<T, RequestConfig> =
        await instance.request<T>(mergedConfig);
      const { data } = response;
      return { err: null, data, response };
    } catch (err: any) {
      return { err, data: null, response: null };
    }
  };
};
export default makeRequest;

上面代码中重点在于MakeRequest类型中对泛型的处理,其他逻辑都很简略。

终究展现一下支撑途径参数替换的拦截器urlArgs对应的代码:

const urlArgsHandler = {
  request: {
    onFulfilled: (config: AxiosRequestConfig) => {
      const { url, args } = config as RequestConfig;
      // 查看config中是否有args特点,没有则跳过以下代码逻辑
      if (args) {
        const lostParams: string[] = [];
        // 运用String.prototype.replace和正则表达式进行匹配替换
        const replacedUrl = url!.replace(/{([^}]+)}/g, (res, arg: string) => {
          if (!args[arg]) {
            lostParams.push(arg);
          }
          return args[arg] as string;
        });
        // 假如url存在未替换的途径参数,则会直接报错
        if (lostParams.length) {
          return Promise.reject(new Error("在args中找不到对应的途径参数"));
        }
        return { ...config, url: replacedUrl };
      }
      return config;
    },
  },
};

已上便是整个二次封装的过程了,假如有不明白的能够直接查看项目 enhance-axios-frame里的代码或在谈论区讨论。

进阶:

我在之前的文章里有介绍怎么给axios附加更多高档功用,假如有兴趣的能够点击以下链接看看:

  1. 反馈恳求成果

  2. 接口限流

  3. 数据缓存

  4. 过错主动重试

后记

这篇文章写到这儿就完毕了,假如觉得有用能够点赞收藏,假如有疑问能够直接在谈论留言,欢迎沟通 。