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

news2024/11/14 15:31:30

目录

前言

Demo

第一种实现方法

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


前言

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

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

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

Demo

​ 项目demo地址:cancelRequest: 避免重复调用接口的demo ​icon-default.png?t=N7T8https://gitee.com/zhp26zhp/cancel-request

第一种实现方法

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

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

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()

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

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函数处理响应,并发布订阅通知

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);
}

 完整代码如下:

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;

调用接口例子

export function getWetherByOneExample() {
  return instance.get('/data')
}

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

  • isCancel:从axios中提取用于判断请求是否被取消的方法。
  • cacheRequest:用于存储请求的AbortController实例,以便后续取消请求。
const { isCancel } = axios;
const cacheRequest = {};
  • abortCacheRequest:根据请求的唯一标识reqKey取消请求。
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中。
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);
  }
);
  • 在响应返回后,如果请求被取消,则从缓存中删除对应的请求。
  • 处理响应错误,如果请求被取消,则返回自定义的错误信息;否则,显示错误消息并返回错误对象。
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);
  }
);

 完整代码如下:

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;

调用例子

export function getWetherByTwoExample() {
  return service({
    url: '/data',
    method: 'get',
    isAbort: true
  })
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2033070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

R语言的下载和安装

R是一种强大的编程语言和环境,主要用于统计计算和数据分析。自从R诞生以来,它在统计学家和数据科学家中迅速普及。然而,R本身只是一个命令行工具,对于大多数用户来说,单独使用R可能会有些困难或者不方便。RStudio作为一…

科技云报道:“大模型+机器人”,具身智能将开启“智械时代”

科技云报道原创。 从15世纪达芬奇绘制出世界上第一份人形机器人手稿,到如今波士顿动力、本田、特斯拉、Figure AI等企业相继推出了人形机器人产品,机器人新物种持续衍生,人形机器人产业已经从萌芽概念阶段进入产业化落地前期。 近日&#x…

Golang面试题四(并发编程)

目录 1.Go常见的并发模型 2.哪些方法安全读写共享变量 3.如何排查数据竞争问题 ​4.Go有哪些同步原语 1. Mutex (互斥锁) 2. RWMutex (读写互斥锁) 3. Atomic 3.1.使用场景 3.2.整型操作 3.3.指针操作 3.4.使用示例 4. Channel 使用场景 使用示例 5. sync.WaitGr…

Java同城生鲜配送物流配送到店独立骑手端系统小程序源码

🚚【一键解锁新鲜生活!同城生鲜配送系统全揭秘】🥦🚀 🔍 源码揭秘:打造高效生鲜配送的秘密武器 🔧 想要在家就能享受超市般的生鲜盛宴吗?揭秘同款城生鲜配送系统的源码&#xff0c…

信号与线性系统实验一:LTI连续系统时域响应测试与分析

文章目录 一、实验目的二、实验内容与原理(简单列了一下提纲)第一部分:连续系统时域响应MATLAB仿真分析第二部分:连续系统时域响应Multisim电路仿真分析 三、实验器材四、实验步骤第一部分:连续系统时域响应MATLAB仿真…

vulnhub系列:sp eric

vulnhub系列:sp eric 靶机下载 一、信息收集 nmap扫描存活,根据mac地址寻找IP nmap 192.168.23.0/24nmap扫描端口,开放端口:22、80 nmap 192.168.23.189 -p- -A -sV -Pndirb 扫描目录,.git 源码,admin…

【python】PyQt5中单行文本输入控件QLineEdit的详细解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…

使用Python打造简易Web服务器

目录 准备工作 创建Web服务器 示例代码 运行服务器 结论 在开发过程中,了解Web服务器的工作原理是非常有用的。Python作为一个功能强大的编程语言,提供了http.server模块,让我们能够快速地搭建一个简易的Web服务器。本文将指导你如何使用…

【微服务】Spring Cloud Alibaba 的介绍以及和主要功能

文章目录 引言主要功能1. **服务发现与注册**2. **分布式配置管理**3. **流量管理与熔断限流**4. **消息驱动**5. **分布式事务管理**6. **远程调用(RPC)**7. **服务网关**8. **对象存储**9. **全链路跟踪**10. **阿里巴巴中间件支持**11. **高可用与容错…

LInux - 一文了解 ssh端口敲门knock

文章目录 基本概念工作原理实操注意事项 基本概念 SSH端口敲门技术是一种网络安全措施,用于防止未经授权的访问。通过端口敲门,可以动态地在防火墙上打开指定端口(如SSH端口),仅允许符合特定敲门序列的用户访问。此技…

算法:魔法字典

1️⃣要求: 设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。 实现 MagicDictionary 类…

Spring中WebSocket的使用

文章目录 前言什么是 WebSocketWebSocket 协议和 HTTP 协议的区别WebSocket 原理解析WebSocket 报文格式 Spring 中 WebSocket 的使用前后端发送的数据的数据类型是对象该如何做使用websocket协议如何获取到HTTP协议中的HttpSession WebSocket使用的完整代码 前言 我们在使用 …

docker基本管理和应用

一、docker是什么: 1.docker是什么:一个开源的应用容器引擎,基于go语言开发的,docker运行在linux的容器化工具,可以理解为轻量级的一个虚拟机。可以在任何主机上轻松创建的一个轻量级、可移植的自给自足的容器&#x…

【Tor】使用Debian系统搭建obfs4 Bridge网桥

你好 我是无聊的木子。 目录 前言 写作の原因 网桥是个啥? 正文 - 到底咋搭建捏 搞台机子先 比较简便の方法 - 买台云服务器 首月五折 一元试用 远程连接服务器 更加复杂の办法 - 自己拿物理机做网桥 开始搭建网桥 先安装Tor 然后配置网桥 最后组合网桥…

【阿旭机器学习实战】【38】支持向量机SVM实现手写数字识别,模型训练、评估,以及参数调优全流程

《------往期经典推荐------》 一、【100个深度学习实战项目】【链接】,持续更新~~ 二、机器学习实战专栏【链接】,已更新31期,欢迎关注,持续更新中~~ 三、深度学习【Pytorch】专栏【链接】 四、【Stable Diffusion绘画系列】专…

React之简易笔记本

此文使用React实现简易笔记本,包括环境配置,前台界面和后台应用等内容。其中后台应用主要功能是数据库操作,前台应用的主要功能是显示,增加,删除,更新数据 ,效果如下所示: 一、数据…

Android Framework之Pkms详解

PKMS是Android系统中负责安装包管理的服务,它的主要职责如下: 管理系统安装的所有应用程序,包括升级、安装、卸载 根据Intent匹配相应的Activity、Service、Provider和BroadcastReceiver等,并提供相关信息 解析应用权限&#xff…

深入探讨进程间通信的重要性:理解不同的通信机制(下)

前言 在上一篇文章中,我们探讨了进程间通信的三种常见机制:管道、消息队列和共享内存。我们了解到,这些机制各有其特点和适用场景,可以根据实际需求选择合适的机制进行进程间通信。然而,进程间通信并不仅限于这三种方…

Zookeeper学习、Tomcat

怎样使用Zookeeper实现服务发现? 典型回答 服务发现是ZK的重要用途之一,当我们想要基于zk实现服务发现时,一般可以参考以下步骤:1. 向Zookeeper注册服务 服务提供者需要在Zookeeper上创建一个临时节点来注册自己的服务。节点的名…

第五届IEEE先进电气和能源系统国际会议(AEES 2024)即将召开!

第五届先进电气和能源系统国际会议将于2024年11月29日至12月1日在中国兰州召开,欢迎参加! 本届会议关注先进电气和能源系统的新理论及其应用,为相关领域的技术及相关研究领域的专家、学者交流最新研究成果、探讨学术发展方向提供一个广泛的交…