axios 封装避免重复请求(两种)

目录

前言

Demo

第一种实现方法

第二种方法(axios版本0.22.0以上)


前言

在当今的前端开发领域,数据交互是不可或缺的一环。Axios 作为一款基于 Promise 的 HTTP 客户端,因其简洁的 API 和丰富的配置选项,深受广大开发者的喜爱。然而,在实际项目中,我们常常会遇到一个问题:重复发送相同的请求,这不仅会浪费网络资源,还可能导致服务器压力增大。为了提高应用的性能和用户体验,我们需要对 Axios 进行封装,以避免重复请求的问题。

本文将详细介绍两种封装 Axios 的方案,帮助您有效地避免重复请求,提升项目质量。我们将从实际场景出发,分析问题原因,并提供具体的实现步骤和代码示例。无论您是初入前端的新手,还是经验丰富的开发者,相信都能从中获得启发。

让我们一起探索如何优雅地封装 Axios,让数据交互更加高效、稳定!

Demo

项目demo地址:cancelRequest: 避免重复调用接口的demo https://gitee.com/zhp26zhp/cancel-request

第一种实现方法

通过生成请求的唯一标识符(key),来避免重复发送相同的请求。(发布订阅模式)

定义了一个EventEmitter类,用于实现发布订阅模式。它允许在请求成功或失败时,通知所有订阅了该请求的回调函数。

javascript 复制代码
class EventEmitter {
    constructor() {
        this.event = {}
    }
    on(type, cbres, cbrej) {
        if (!this.event[type]) {
            this.event[type] = [[cbres, cbrej]]
        } else {
            this.event[type].push([cbres, cbrej])
        }
    }

    emit(type, res, ansType) {
        if (!this.event[type]) return
        else {
            this.event[type].forEach(cbArr => {
                if(ansType === 'resolve') {
                    cbArr[0](res)
                }else{
                    cbArr[1](res)
                }
            });
        }
    }
}

生成请求Key

javascript 复制代码
function generateReqKey(config, hash) {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

存储已发送但未响应的请求

javascript 复制代码
const pendingRequest = new Set();
const ev = new EventEmitter()

请求拦截器中,在请求发送前,生成请求Key,并检查是否已有相同的请求在等待响应。如果有,则通过发布订阅模式挂起该请求,直到收到响应。如果没有,则将请求Key添加到pendingRequest中。

javascript 复制代码
instance.interceptors.request.use(async (config) => {
    let hash = location.hash
    let reqKey = generateReqKey(config, hash)
    
    if(pendingRequest.has(reqKey)) {
        let res = null
        try {
          res = await new Promise((resolve, reject) => {
                    ev.on(reqKey, resolve, reject)
                })
          return Promise.reject({
                    type: 'limiteResSuccess',
                    val: res
                })
        }catch(limitFunErr) {
            return Promise.reject({
                        type: 'limiteResError',
                        val: limitFunErr
                    })
        }
    }else{
        config.pendKey = reqKey
        pendingRequest.add(reqKey)
    }

    return config;
  }, function (error) {
    return Promise.reject(error);
  });

响应拦截器中,在请求成功或失败时,通过handleSuccessResponse_limithandleErrorResponse_limit函数处理响应,并发布订阅通知

javascript 复制代码
instance.interceptors.response.use(function (response) {
    handleSuccessResponse_limit(response)
    return response;
  }, function (error) {
    return handleErrorResponse_limit(error)
  });

将成功响应的结果发布给所有订阅了该请求的回调函数。

javascript 复制代码
function handleSuccessResponse_limit(response) {
      const reqKey = response.config.pendKey
    if(pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(response))
      }catch(e) {
        x = response
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'resolve')
      delete ev.reqKey
    }
}

将错误响应的结果发布给所有订阅了该请求的回调函数。

javascript 复制代码
function handleErrorResponse_limit(error) {
    if(error.type && error.type === 'limiteResSuccess') {
      return Promise.resolve(error.val)
    }else if(error.type && error.type === 'limiteResError') {
      return Promise.reject(error.val);
    }else{
      const reqKey = error.config.pendKey
      if(pendingRequest.has(reqKey)) {
        let x = null
        try {
          x = JSON.parse(JSON.stringify(error))
        }catch(e) {
          x = error
        }
        pendingRequest.delete(reqKey)
        ev.emit(reqKey, x, 'reject')
        delete ev.reqKey
      }
    }
      return Promise.reject(error);
}

完整代码如下:

javascript 复制代码
import axios from "axios"

let instance = axios.create({
    baseURL: 'http://localhost:3001', // api 的 base_url
    timeout: 50e3, // request timeout
})

// 发布订阅
class EventEmitter {
    constructor() {
        this.event = {}
    }
    on(type, cbres, cbrej) {
        if (!this.event[type]) {
            this.event[type] = [[cbres, cbrej]]
        } else {
            this.event[type].push([cbres, cbrej])
        }
    }

    emit(type, res, ansType) {
        if (!this.event[type]) return
        else {
            this.event[type].forEach(cbArr => {
                if(ansType === 'resolve') {
                    cbArr[0](res)
                }else{
                    cbArr[1](res)
                }
            });
        }
    }
}


// 根据请求生成对应的key
function generateReqKey(config, hash) {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()

// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
    let hash = location.hash
    // 生成请求Key
    let reqKey = generateReqKey(config, hash)
    
    if(pendingRequest.has(reqKey)) {
        // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
        // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
        let res = null
        try {
            // 接口成功响应
          res = await new Promise((resolve, reject) => {
                    ev.on(reqKey, resolve, reject)
                })
          return Promise.reject({
                    type: 'limiteResSuccess',
                    val: res
                })
        }catch(limitFunErr) {
            // 接口报错
            return Promise.reject({
                        type: 'limiteResError',
                        val: limitFunErr
                    })
        }
    }else{
        // 将请求的key保存在config
        config.pendKey = reqKey
        pendingRequest.add(reqKey)
    }

    return config;
  }, function (error) {
    return Promise.reject(error);
  });

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response)
    return response;
  }, function (error) {
    return handleErrorResponse_limit(error)
  });

// 接口响应成功
function handleSuccessResponse_limit(response) {
      const reqKey = response.config.pendKey
    if(pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(response))
      }catch(e) {
        x = response
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'resolve')
      delete ev.reqKey
    }
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
    if(error.type && error.type === 'limiteResSuccess') {
      return Promise.resolve(error.val)
    }else if(error.type && error.type === 'limiteResError') {
      return Promise.reject(error.val);
    }else{
      const reqKey = error.config.pendKey
      if(pendingRequest.has(reqKey)) {
        let x = null
        try {
          x = JSON.parse(JSON.stringify(error))
        }catch(e) {
          x = error
        }
        pendingRequest.delete(reqKey)
        ev.emit(reqKey, x, 'reject')
        delete ev.reqKey
      }
    }
      return Promise.reject(error);
}

export default instance;

调用接口例子

javascript 复制代码
export function getWetherByOneExample() {
  return instance.get('/data')
}

第二种方法(axios版本0.22.0以上)

  • isCancel:从axios中提取用于判断请求是否被取消的方法。
  • cacheRequest:用于存储请求的AbortController实例,以便后续取消请求。
javascript 复制代码
const { isCancel } = axios;
const cacheRequest = {};
  • abortCacheRequest:根据请求的唯一标识reqKey取消请求。
javascript 复制代码
function abortCacheRequest(reqKey) {
  if (cacheRequest[reqKey]) {
    console.log("abortCacheRequest", reqKey);
    cacheRequest[reqKey].abort();
    delete cacheRequest[reqKey];
    console.log("abortCacheRequest", cacheRequest);
  }
}
  • 在请求发送前,检查config中是否包含isAbort字段,如果为true,则取消之前的相同请求。
  • 使用AbortController来取消请求,并将signal属性添加到config中。
javascript 复制代码
service.interceptors.request.use(
  (config) => {
    const { url, method, isAbort = false } = config;
    if (isAbort) {
      const reqKey = `${url}&${method}`;
      abortCacheRequest(reqKey);
      const controller = new AbortController();
      config.signal = controller.signal;
      cacheRequest[reqKey] = controller;
    }
    return config;
  },
  (error) => {
    console.log(error); // for debug
    return Promise.reject(error);
  }
);
  • 在响应返回后,如果请求被取消,则从缓存中删除对应的请求。
  • 处理响应错误,如果请求被取消,则返回自定义的错误信息;否则,显示错误消息并返回错误对象。
javascript 复制代码
service.interceptors.response.use(
  (response) => {
    const { url, method, isAbort = false } = response.config;
    if (isAbort) delete cacheRequest[`${url}&${method}`];
    const res = response.data;
    return res;
  },
  (error) => {
    if (isCancel(error)) {
      return Promise.reject({
        message: "重复请求,已取消",
      });
    }
    console.log("err" + error); // for debug
    Message({
      message: "登录连接超时(后台不能连接,请联系系统管理员)",
      type: "error",
      duration: 5 * 1000,
    });
    error.data = { msg: "系统内部错误,请联系管理员维护" };
    return Promise.reject(error);
  }
);

完整代码如下:

javascript 复制代码
import axios from "axios";

const service = axios.create({
  baseURL: "http://localhost:3001", // api 的 base_url
  timeout: 500000, // request timeout
});

// isAbort Start
const { isCancel } = axios;
const cacheRequest = {};
// 删除缓存队列中的请求
function abortCacheRequest(reqKey) {
  if (cacheRequest[reqKey]) {
    // 通过AbortController实例上的abort来进行请求的取消
    console.log("abortCacheRequest", reqKey);
    cacheRequest[reqKey].abort();
    delete cacheRequest[reqKey];
    console.log("abortCacheRequest", cacheRequest);
  }
}
// isAbort End

// request interceptor
service.interceptors.request.use(
  (config) => {
    // isAbort Start
    const { url, method, isAbort = false } = config;
    if (isAbort) {
      // 请求地址和请求方式组成唯一标识,将这个标识作为取消函数的key,保存到请求队列中
      const reqKey = `${url}&${method}`;
      // 如果config传了需要清除重复请求的isAbort,则如果存在重复请求,删除之前的请求
      abortCacheRequest(reqKey);
      // 将请求加入请求队列,通过AbortController来进行手动取消
      const controller = new AbortController();
      config.signal = controller.signal;
      cacheRequest[reqKey] = controller;
    }
    // isAbort End

    return config;
  },
  (error) => {
    // Do something with request error
    console.log(error); // for debug
    Promise.reject(error);
  }
);

// response interceptor
service.interceptors.response.use(
  (response) => {
    // isAbort Start
    const { url, method, isAbort = false } = response.config;
    if (isAbort) delete cacheRequest[`${url}&${method}`];
    // isAbort End

    const res = response.data;

    return res;
  },
  (error) => {
    if (isCancel(error)) {
      // 通过AbortController取消的请求不做任何处理
      return Promise.reject({
        message: "重复请求,已取消",
      });
    }
    console.log("err" + error); // for debug
    Message({
      message: "登录连接超时(后台不能连接,请联系系统管理员)",
      type: "error",
      duration: 5 * 1000,
    });
    error.data = { msg: "系统内部错误,请联系管理员维护" };
    return Promise.reject(error);
  }
);

export default service;

调用例子

javascript 复制代码
export function getWetherByTwoExample() {
  return service({
    url: '/data',
    method: 'get',
    isAbort: true
  })
}

项目demo地址:cancelRequest: 避免重复调用接口的demo

相关推荐
小曲曲23 分钟前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
----云烟----4 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言