Axios封装

2024-08 1

1. 为什么要封装Axios以及封装的注意点?

封装目的:

  1. 统一get、post请求方式。
  2. 统一处理请求和响应、错误处理。否则每个需要用到请求的地方,都需要使用try-catch捕捉错误。
  3. 方便后续拓展。封装了Axios后,后续如果有涉及到接口层的改动,比如说需要在调用的时候增加鉴权字段,增加防重放攻击等,只改一个地方即可。

注意点:

  1. 对特定 method 封装成新的 API,却暴露极少的参数,如封装GET、POST方法,确只暴露url和param或者data。
  2. 封装创建axios实例的方法,或者封装自定义axios类。

以上两点在封装的时候应该避免,因为这样会增加理解成本,属于为了封装而封装。

2. 安装 Axios

首先,确保你的项目中已经安装了 Axios。如果还没有安装,可以使用 npm 或 yarn 进行安装。

npm install axios
# or
yarn add axios

3. 创建 Axios 实例

新建http.js文件,通过Axios.create创建一个Axios实例 :

// 创建一个 Axios 实例
const instance = axios.create({
  baseURL: "https://api.example.com", // API的基础路径
  timeout: 10000, // 请求超时时间
  headers: {
    "Content-Type": "application/json",
    // 其他全局默认请求头
  },
});

4. 封装请求拦截器

到这里需要注意一下,由于Axios的get请求和Post请求对于参数的处理稍微有点不同,因此我们可以在封装请求拦截器的时候,统一它们俩的请求方式。

原始的get、post请求方式如下:

// GET 请求方法
axiso.get(url, { params, headers });

// 封装 POST 请求方法
axiso.post(url, data, { headers });

统一之后如下:

// GET 请求方法
axiso.get(url, { params, headers });

// 封装 POST 请求方法
axiso.post(url, { data, headers });

封装拦截器如下:

// 请求拦截器
instance.interceptors.request.use((config) => {
  if (config.method === "post") {
    const data = config.data;
    config = {
      ...config,
      ...data,
      data: data.data,
    };
  }
  return config;
});

5.封装响应拦截器

const codeMessage = {
  200: "服务器成功返回请求的数据。",
  201: "新建或修改数据成功。",
  202: "一个请求已经进入后台排队(异步任务)。",
  204: "删除数据成功。",
  400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
  401: "用户没有权限(令牌、用户名、密码错误)。",
  403: "用户得到授权,但是访问是被禁止的。",
  404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
  406: "请求的格式不可得。",
  410: "请求的资源被永久删除,且不会再得到的。",
  422: "当创建一个对象时,发生一个验证错误。",
  500: "服务器发生错误,请检查服务器。",
  502: "网关错误。",
  503: "服务不可用,服务器暂时过载或维护。",
  504: "网关超时。",
};

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 处理响应数据
    return response.data;
  },
  (error) => {
    // 统一处理响应错误
    if (error.response) {
      // 服务器响应了错误代码
      const errorMsg = codeMessage[error.response.status];
      console.log(errorMsg);
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      console.error("网络错误");
    } else {
      // 处理其他错误
      console.error("请求错误", error.message);
    }
    return Promise.resolve(error);
  },
);

注意,在上述代码中,第38行return Promise.resolve(error);通过Promise.resolve()方法将错误返回给调用方,这样,在调用方的时候就需要根据状态码进行处理即可,无需进行错误处理。

6. 使用封装的 Axios 实例

封装完后,我们在http文件中导出instance实例:

const { get, post } = instance;
export { get, post };
export default instance;

这样就可以在项目的其他部分直接使用封装好的 instance 进行 API 请求。

import { get, post } from './http.js'

// GET 请求
const res = await get("/demo", { params: { id: 123 } });

// POST 请求
const res = await post("/demo", { data: {id: 123 } });

7. 处理不同环境下的 Base URL

如果项目有多个环境(如开发、测试、生产),可以通过不同环境配置不同的 baseURL

// 在 http.js 中
const instance = axios.create({
  baseURL: process.env.REACT_APP_API_URL || "https://api.example.com",
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

确保在 .env 文件中为不同的环境配置了 REACT_APP_API_URL

# .env.development
REACT_APP_API_URL=https://dev.api.example.com

# .env.production
REACT_APP_API_URL=https://prod.api.example.com

通过这种方式,你可以灵活管理不同环境下的 API 请求。

以上,便是一次简单的Axios封装。接下来,我们继续封装,增加重试机制、取消请求机制、防重放攻击机制、

8. 添加重试机制

重试机制需要用到 axios-retry 插件。

(1)安装axios-retry

pnpm i axios-retry --save

(2)为axios实例添加重试机制

// 添加重试机制
axiosRetry(instance, {
  retries: 3, // 设置重试次数
  retryDelay: (retryCount) => {
    return retryCount * 1000; // 设置重试间隔(每次重试延迟增加)
  },
  retryCondition: (error: any) => {
    // 指定在什么条件下重试,比如5xx错误
    return error?.status > 500;
  },
});

9. 封装取消请求

先来看看单个请求怎么取消:

import axios from "axios";
import { get } from './http.js'

const { token, cancel } = axios.CancelToken.source();
get("/demo", { cancelToken: token }).then((res) => {
  console.log(res);
});
setTimeout(() => {
  cancel();
}, 5);

具体逻辑可以看我以前写的一篇文章:前端中断请求的方式与原理

现在来看看封装批量取消请求:

// 1. 创建一个 Map 来存储取消函数
const cancelTokenMap = new Map();

// 请求拦截器
instance.interceptors.request.use((config) => {
  // 2. 生成取消请求的Token
  const { token, cancel } = axios.CancelToken.source();
  config.cancelToken = token;

  // 3. 生成一个唯一标识符,例如使用请求的 URL 作为 key,并存储取消函数到 Map
  cancelTokenMap.set( config.url, cancel);

  if (config.method === "post") {
    const data = config.data;
    config = {
      ...config,
      ...data,
      data: data.data,
    };
  }

  return config;
});

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 4. 请求成功后,从 Map 中移除该请求的取消函数
    cancelTokenMap.delete(response.config.url);

    // 处理响应数据
    return response;
  },
  (error) => {
    // 5. 请求失败也要移除取消函数
    if (axios.isCancel(error)) {
      console.log("请求被取消:", error.message);
    } else {
      cancelTokenMap.delete(error.config.url);
    }

    // 统一处理响应错误
    if (error.response) {
      // 服务器响应了错误代码
      const errorMsg = codeMessage[error.response.status];
      console.log(errorMsg);
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      console.error("网络错误");
    } else {
      // 处理其他错误
      console.error("请求错误", error.message);
    }
    return Promise.resolve(error);
  }
);

// 6. 封装批量取消请求的函数
function cancelRequests(requestIds = []) {
  if (requestIds.length > 0) {
    // 取消指定的请求
    requestIds.forEach((requestId) => {
      if (cancelTokenMap.has(requestId)) {
        cancelTokenMap.get(requestId)(); // 调用取消函数
        cancelTokenMap.delete(requestId); // 移除该取消函数
      }
    });
  } else {
    // 如果没有指定请求 ID,则取消所有请求
    cancelTokenMap.forEach((cancel) => cancel());
    cancelTokenMap.clear();
  }
}

const { get, post } = instance;
export { get, post, cancelRequests };
export default instance;

上述代码中,我们添加了批量请求方式,具体改动如下:

  1. 创建一个 Map 来存储取消函数。
  2. 为每个请求创建一个取消令牌。
  3. 使用请求的 URL 作为唯一标识符,并存储取消函数到 Map,这样后续可以通过传递请求的URL来取消请求。当然,在多个请求的URL相同而参数不同时,取消的就是最后一个发起的URL。
  4. 请求成功后,从 Map 中移除该请求的取消函数。
  5. 请求失败也要移除取消函数。
  6. 封装批量取消请求的函数。

使用方式如下:

// 取消特定请求
cancelRequests(['/demo', '/another-endpoint']);

// 取消所有请求
cancelRequests();

10. 添加防重放攻击机制

重放攻击,就是同一个请求被黑客重复触发,导致数据库多次更新。比如黑客抓包到存钱的接口,然后不断触发接口,那么账户的金额就会不断增加。

要想实现防重放攻击,也就是避免同一个请求被出发两次,就需要给每一个请求打上唯一标识。然后后端维护一个已接收的请求标识符列表(通常有时效性),确保相同标识符不会被重复使用。

import { v4 as uuidv4 } from 'uuid';  // 使用 uuid 来生成唯一标识符

// 请求拦截器
instance.interceptors.request.use((config) => {
	// 其他代码...
  // 生成防重放攻击的唯一标识符,具体字段名视实际情况而定
  config.headers["X-Request-Nonce"] = uuidv4();

  // 其他代码...
});

这里需要注意,只使用uuidv4并不安全,最好是将uuidv4进行加密。

11. 完整代码:

import axios from "axios";
import axiosRetry from "axios-retry";
import { v4 as uuidv4 } from "uuid";

// 创建一个 Axios 实例
const instance = axios.create({
  baseURL: "/api", // API的基础路径
  timeout: 10000, // 请求超时时间
  headers: {
    "Content-Type": "application/json",
    // 其他全局默认请求头
  },
});

const codeMessage = {
  200: "服务器成功返回请求的数据。",
  201: "新建或修改数据成功。",
  202: "一个请求已经进入后台排队(异步任务)。",
  204: "删除数据成功。",
  400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
  401: "用户没有权限(令牌、用户名、密码错误)。",
  403: "用户得到授权,但是访问是被禁止的。",
  404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
  406: "请求的格式不可得。",
  410: "请求的资源被永久删除,且不会再得到的。",
  422: "当创建一个对象时,发生一个验证错误。",
  500: "服务器发生错误,请检查服务器。",
  502: "网关错误。",
  503: "服务不可用,服务器暂时过载或维护。",
  504: "网关超时。",
};

// 添加重试机制
axiosRetry(instance, {
  retries: 3, // 设置重试次数
  retryDelay: (retryCount) => {
    return retryCount * 1000; // 设置重试间隔(每次重试延迟增加)
  },
  retryCondition: (error: any) => {
    // 指定在什么条件下重试,比如5xx错误
    return error?.status > 500;
  },
});

// 创建一个 Map 来存储取消函数
const cancelTokenMap = new Map();

// 请求拦截器
instance.interceptors.request.use((config) => {
  // 生成取消请求的Token
  const { token, cancel } = axios.CancelToken.source();
  config.cancelToken = token;

  // 使用请求的 URL 作为唯一标识符,并存储取消函数到 Map
  cancelTokenMap.set(config.url, cancel);

  // 生成防重放攻击的唯一标识符,具体字段名视实际情况而定
  config.headers["X-Request-Nonce"] = uuidv4();

  if (config.method === "post") {
    const data = config.data;
    config = {
      ...config,
      ...data,
      data: data.data,
    };
  }

  return config;
});

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 请求成功后,从 Map 中移除该请求的取消函数
    cancelTokenMap.delete(response.config.url);

    // 处理响应数据
    return response;
  },
  (error) => {
    // 请求失败也要移除取消函数
    if (axios.isCancel(error)) {
      console.log("请求被取消:", error.message);
    } else {
      cancelTokenMap.delete(error.config.url);
    }

    // 统一处理响应错误
    if (error.response) {
      // 服务器响应了错误代码
      const errorMsg = codeMessage[error.response.status];
      console.log(errorMsg);
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      console.error("网络错误");
    } else {
      // 处理其他错误
      console.error("请求错误", error.message);
    }
    return Promise.resolve(error);
  }
);

// 封装批量取消请求的函数
function cancelRequests(requestIds = []) {
  if (requestIds.length > 0) {
    // 取消指定的请求
    requestIds.forEach((requestId) => {
      if (cancelTokenMap.has(requestId)) {
        cancelTokenMap.get(requestId)(); // 调用取消函数
        cancelTokenMap.delete(requestId); // 移除该取消函数
      }
    });
  } else {
    // 如果没有指定请求 ID,则取消所有请求
    cancelTokenMap.forEach((cancel) => cancel());
    cancelTokenMap.clear();
  }
}

const { get, post } = instance;
export { get, post, cancelRequests };
export default instance;