axios二次封装-2023

该文介绍了如何对axios进行二次封装,重点在于处理重复请求(选择保留第一个请求)和实现请求自动重试机制。还提供了取消所有请求的功能,并根据环境改变baseUrl。文中包含了封装的request.js代码示例,包括请求和响应拦截器的实现,以及get、post等方法的封装。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

axios二次封装

前言

提示功能用的是element-ui,大家可根据实际使用的ui库进行替换

开始封装之前,翻了官方文档,看了很多博文,总结如下:

  • 不需要过度封装
  • 重点关注“请求拦截”和“响应拦截”
  • 处理重复请求机制(保留第一个请求 or 保留最后一个请求)
  • 自动重试请求的时机
  • 封装同时也要保留好拓展性

亮点

  • 处理重复请求(只发送第一个请求)
  • 请求自动重试(重试时机:超时触发)
  • 清空全部请求(使用场景:切换页面后,停止上一个页面的所有请求)
  • 封装 get post put delete upload 请求
  • 根据不同环境更改不同 baseUrl

⚠️ 处理重复请求有两种选择:

1️⃣ 第一种:保留第一个请求,后续相同请求全部取消

2️⃣ 第二种:保留最后一个请求,前面相同请求全部取消

🌈 总结:实际请求发现第二种,只保留最后一个请求,前面发出去的请求就算前端取消了,后端日志显示还是收到请求,这与我们的预期不符合,故使用第一种方案。

request.js

'use strict';

import axios from 'axios';

import { Message } from 'element-ui';

/**
 * [汇总] 自定义 config 属性
 * allowDuplicateRequest {boolean} false 允许重复请求
 * restoreDataFormat {boolean} true 还原数据结构
 * successText {string} '' 成功提示文本
 * errorText {string} '' 失败提示文本
 * retry {number} 3 重试次数
 * retryDelay {number} 2 重试时间(单位s)
 */

const axiosInstance = axios.create({
  baseURL: '/',
  timeout: 3 * 1000,
});

// 根据不同环境更改不同 baseUrl
if (process.env.NODE_ENV === 'development') {
  axiosInstance.defaults.baseURL = '/'; // 开发环境
} else if (process.env.NODE_ENV === 'production') {
  axiosInstance.defaults.baseURL = '/'; // 生产环境
}

// 请求队列
const requestQueue = new Map();

// 请求拦截器
axiosInstance.interceptors.request.use((config) => {

  // 请求之前做些什么
  // 例如:请求头添加 token

  // console.log('请求拦截', config);

  const { allowDuplicateRequest = false } = config;

  // 允许重复请求
  if (allowDuplicateRequest) {
    return config;
  }



  /**
   * 以下逻辑是处理 => 重复请求
   */

  // 生成请求key
  const requestKey = getRequestKey(config);

  // 创建请求控制器
  const controller = new AbortController();

  config.signal = controller.signal;

  // 判断请求队列是否存在相同请求
  if (requestQueue.has(requestKey)) {

    // 停止请求
    controller.abort();
  } else {
    // 把本次请求提交到队列
    requestQueue.set(requestKey, controller);
  }



  return config;
}, (error) => {

  // 请求错误

  console.log('请求错误', error);

  return Promise.reject(error);
});

// 响应拦截器
axiosInstance.interceptors.response.use((response) => {

  // 响应 status === 200

  // console.log('响应拦截', response);

  const config = response.config;

  // 生成请求key
  const requestKey = getRequestKey(config);
  // 从请求队列移除掉
  requestQueue.delete(requestKey);

  // 还原数据结构, 成功提示文本, 失败提示文本
  const { restoreDataFormat = true, successText, errorText } = config;

  // 还原数据结构
  if (restoreDataFormat) {

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

    if (code === 200) {

      Message({
        message: successText || msg || '成功',
        type: 'success'
      });
    } else {

      Message({
        message: errorText || msg || '失败',
        type: 'error'
      });


      return Promise.reject(msg)
    }

    return data;
  }



  return response;
}, async (error) => {

  // 响应 status !== 200

  console.log('响应错误', error);

  const config = error.config;



  /**
   * 以下逻辑处理 => 请求重试
   * 重试时机: 超时
   */

  if (error.message.indexOf('timeout') !== -1) {
    // 生成请求key
    const requestKey = getRequestKey(config);
    // 从请求队列移除掉
    requestQueue.delete(requestKey);

    // 重试次数、重试时间、当前重试次数
    const { retry = 3, retryDelay = 2, retryCount = 0 } = config;

    // 延时处理
    const delay = new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, retryDelay * 1000);
    });

    // 重试次数 && 当前重试次数未超过最大重试次数
    if (retry && (retryCount < retry)) {

      // 当前重试次数加一
      config.retryCount = retryCount + 1;

      console.log(`[请求异常] 尝试重新请求 => [${config.url}] => 第 ${config.retryCount}`);

      Message({
        message: `[请求异常] 尝试重新请求第 ${config.retryCount}`,
        type: 'warning'
      });

      // 重新发起请求
      return delay.then(function () {
        return baseRequest(config);
      });
    }

    Message({
      message: '接口异常, 请联系管理员',
      type: 'error'
    });
  }



  return Promise.reject(error);
});

// 基础请求
const baseRequest = (config) => {

  return axiosInstance(config);
}

const get = (url = '', params = {}, config = {}) => {

  const newConfig = Object.assign({}, {
    method: 'get',
    url,
    params,
  }, config);

  return baseRequest(newConfig)
}

const post = (url = '', data = {}, config = {}) => {

  const newConfig = Object.assign({}, {
    method: 'post',
    url,
    data,
  }, config);

  return baseRequest(newConfig)
}

const put = (url = '', data = {}, config = {}) => {

  const newConfig = Object.assign({}, {
    method: 'put',
    url,
    data,
  }, config);

  return baseRequest(newConfig)
}

const destroy = (url = '', data = {}, config = {}) => {

  const newConfig = Object.assign({}, {
    method: 'delete',
    url,
    data,
  }, config);

  return baseRequest(newConfig)
}

const upload = (url = '', data = {}, config = {}) => {

  const newConfig = Object.assign({}, {
    method: 'post',
    url,
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    timeout: 60 * 1000,
  }, config);

  return baseRequest(newConfig)
}

// 字符串转hash
const strToHash = (str) => {
  let hash = 0;
  let chr;

  if (str.length === 0) {
    return hash
  }

  for (let i = 0; i < str.length; i++) {
    chr = str.charCodeAt(i);
    hash = (hash << 5) - hash + chr;
    hash |= 0;
  }

  return hash;
};

const getRequestKey = (config) => {
  let { url, method, data, params } = config;

  data = (typeof data === 'string') ? JSON.parse(data) : data;

  // // 把请求转为字符串
  const requestStr = `${method}_${url}_${params ? JSON.stringify(params) : JSON.stringify(data)}`;

  // // 设置请求key
  const requestKey = strToHash(requestStr);

  return requestKey;
}

const cancelAllRequest = () => {
  requestQueue.forEach((controller) => {
    // 停止请求
    controller.abort();
  });

  requestQueue.clear();

  return 'ok';
}



export default {
  axiosInstance,
  get,
  post,
  put,
  delete: destroy,
  upload,
  cancelAllRequest,
}

get post put delete算是过度封装了,考虑到大家使用场景不同,就保留了下来,觉得不需要的删的就好

使用

上传文件示例

files是数组,可上传多个文件,同时有上传回调,大家可以根据场景添加上传进度

<template>
  <div id="app">
    <input type="file" id="upload" />
    <button @click="uploadFile()">上传文件</button>
  </div>
</template>

<script>
import request from "./request";

export default {
  name: "App",
  components: {},
  mounted() {
  },
  methods: {
    async uploadFile() {
      const input = document.querySelector("#upload");
      const files = input.files;

      const data = await request.upload(
        "http://api.com/upload",
        {
          id: 123,
          files,
        },
        {
          onUploadProgress: function (progressEvent) {
            const { loaded, total, progress, estimated, rate } = progressEvent;

            console.log("文件总大小: ", parseInt(total / 1024));
            console.log("已上传文件大小: ", parseInt(loaded / 1024));
            console.log("当前进度: ", parseInt(progress * 100));
            console.log("预估完成时间: ", parseInt(estimated || 0));
            console.log("当前上传速度: ", parseInt((rate || 0) / 1024));
          },
        }
      );
      console.log("data: ", data);
    },
  },
};
</script>

<style>
</style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值