如何封装一个好用的网络请求(三)-- 重复请求取消

引言

这一系列文章记录了我多年工作中积累的心得体会,旨在与大家分享经验,欢迎各位大佬指导,共同提升技术水平。

承接上篇

前文(如何封装一个好用的网络请求(二)-- 错误统一处理)已经介绍了如何统一的处理错误的请求,目前就已经可以用于大型项目了,本篇将讨论如何进一步优化,具体来说,就是如何缓存请求结果并取消重复请求,以返回缓存的结果。

代码仓库在 案例一:网络封装 · Duck/EmpiricalCase - 码云 - 开源中国 (gitee.com),可以下载下来看完整代码

一、作用

重复请求优化有以下几个作用,可以根据项目需求来决定是否引入:

  • 减轻服务器压力:假如有 10 个相同的请求,服务器需要开 10 个线程处理它们。实际上,只需要处理第一个请求即可,这样可以极大地减轻服务器的压力。

  • 提高用户体验:用户只需要等待第一个请求的时间即可得到响应,显著提升体验。

二、适用场景

在 Vue 的组件开发中,例如将文章项写成一个组件,然后在列表中循环生成 10 个该组件,组件内部通过接口获取数据字段。如果没有取消重复请求的机制,就会同时发送 10 个请求。

在这种情况下,只需要等待第一个请求完成,其他请求复用第一个请求返回的结果即可。

三、思路

  1. 在请求前做一层拦截,将请求放入一个容器内,将请求的 路径 + 请求方式 + 请求参数 作为 key。
  2. 后续请求进入前检查是否存在相同的 key,如果有,代表有重复请求正在进行,此时等待。
  3. 第一个请求完成后,将其结果缓存起来,并分发给其他等待中的请求,然后删除该 key,完成请求。

我们提取关键词:拦截器代表请求前拦截器,完成后代表响应拦截器,还有一个存储等待请求的容器

四、代码

我们在 src/utils/request下面新建一个文件 repeatRequest.js,实现重复请求相关的逻辑。

1. 工具函数:生成请求key

js 复制代码
// 用于根据当前请求的信息,生成请求 Key;
function generateReqConfig(config) {
  const { type, url, data } = config; // 请求方式,参数,请求地址,
  if (data instanceof FormData) {
    return;
  }
  const dataStr = JSON.stringify(data);
  return [url, type, dataStr].join(',');
}

2. 设置请求中容器,并添加 get、set、delete方法

js 复制代码
// 记录请求中的请求,url为key,axiosConfig作为值,请求后的值作为value
const pendingRequest = new Map();

/**
 *添加进请求中的对象中
 * @param {*} config
 * @returns
 */
function addPendingRequest(config) {
  const requestKey = generateReqConfig(config);
  if (requestKey && !pendingRequest.has(requestKey)) {
    pendingRequest.set(requestKey, null);
  }
  console.log('添加请求中请求', pendingRequest);
}

/**
 * 设置请求中的对象,把成功请求放进去
 */
function setSuccessRequest(config, data) {
  const requestKey = generateReqConfig(config);
  pendingRequest.set(requestKey, data);
  console.log('请求成功,设置数据', pendingRequest);

  // 1s后,删除请求中数据,释放内存
  setTimeout(() => {
    deletePendingRequest(config, requestKey);
  }, 500);
}

/**
 * 删除请求中数据
 */
function deletePendingRequest(config, requestKey) {
  if (!requestKey) {
    requestKey = generateReqConfig(config);
  }
  console.log('删除数据', pendingRequest);
  pendingRequest.delete(requestKey);
}

/**
 * 获取成功的请求参数
 * @param {*} config
 * @param {*} requestKey
 * @returns
 */
function getSuccessRequest(config, requestKey) {
  if (!requestKey) {
    requestKey = generateReqConfig(config);
  }
  console.log('获取数据', pendingRequest);
  return pendingRequest.get(requestKey);
}

3. 调整请求函数

  1. 在请求前,添加进请求中容器,后续请求等待,设置定时器,每隔300ms获取一遍是否有结果返回
js 复制代码
/**
 * 请求拦截器
 * @param {Object} config
 * @return false 表示取消请求
 */
function requestInterceptor(config) {
  // 添加进请求等待容器
  addPendingRequest(config);

  return config;
}
js 复制代码
function ajaxFunc(config) {
  const { url, type = 'get', data, headers } = config;
  const realUrl = baseUrl + url;

  return new Promise((resolve, reject) => {
    // 判断重复请求
    let timer = null;
    const requestKey = generateReqConfig(config);
    if (requestKey && pendingRequest.has(requestKey)) {
      // 再次循环查询第一个请求的是否返回数据回来,返回有三种
      // 1. undefined,标识没有这个请求中的请求了,那么代表第一个请求是错误的,直接返回报错回去
      // 2. null,表示第一个请求还在请求中
      // 3. 有值,表示第一个成功,直接服用数据
      timer = setInterval(() => {
        const data = getSuccessRequest(config, requestKey);
        if (data === undefined) {
          clearInterval(timer);
          reject();
        }
        if (data) {
          clearInterval(timer);
          resolve(data);
        }
      }, 300);
      return;
    }
    xxx之前写的逻辑xxxx
    
    
  1. 成功拦截器设置成功参数,错误拦截器删除key
js 复制代码
      success: async function (responseData) {
        // 响应拦截器:请求成功
        try {
          const result = await responseInterceptorSuccess(responseData);
          // 请求成功,设置请求中数据
          setSuccessRequest(config, result);
          resolve(result);
        } catch (error) {
          deletePendingRequest(config);
          reject(error);
        }
      },
      error: function (response) {
        //  响应拦截器:请求失败
        responseInterceptorError(response);
        deletePendingRequest(config);
        // 把响应数据返回出去,页面如果要处理,可以获取
        reject(response.responseJSON);
      },

全部代码

js 复制代码
/**
 * 执行AJAX请求
 * @param {string} url 请求的URL
 * @param {string} type 请求类型(如GET、POST等)
 * @param {Object|string} data 要发送的数据
 * @returns {Promise} 返回一个Promise对象,成功时resolve响应结果,失败时reject错误对象
 */
const baseUrl = 'http://localhost:8080';
function ajaxFunc(config) {
  const { url, type = 'get', data, headers } = config;
  const realUrl = baseUrl + url;

  return new Promise((resolve, reject) => {
    // 判断重复请求
    let timer = null;
    const requestKey = generateReqConfig(config);
    if (requestKey && pendingRequest.has(requestKey)) {
      // 再次循环查询第一个请求的是否返回数据回来,返回有三种
      // 1. undefined,标识没有这个请求中的请求了,那么代表第一个请求是错误的,直接返回报错回去
      // 2. null,表示第一个请求还在请求中
      // 3. 有值,表示第一个成功,直接服用数据
      timer = setInterval(() => {
        const data = getSuccessRequest(config, requestKey);
        if (data === undefined) {
          clearInterval(timer);
          reject();
        }
        if (data) {
          clearInterval(timer);
          resolve(data);
        }
      }, 300);
      return;
    }

    // 请求拦截器
    if (requestInterceptor(config) === false) {
      return reject();
    }
    $.ajax({
      url: realUrl,
      type,
      data,
      success: async function (responseData) {
        // 响应拦截器:请求成功
        try {
          const result = await responseInterceptorSuccess(responseData);
          // 请求成功,设置请求中数据
          setSuccessRequest(config, result);
          resolve(result);
        } catch (error) {
          deletePendingRequest(config);
          reject(error);
        }
      },
      error: function (response) {
        //  响应拦截器:请求失败
        responseInterceptorError(response);
        deletePendingRequest(config);
        // 把响应数据返回出去,页面如果要处理,可以获取
        reject(response.responseJSON);
      },
    });
  });
}

/**
 * 请求拦截器
 * @param {Object} config
 * @return false 表示取消请求
 */
function requestInterceptor(config) {
  addPendingRequest(config);

  return config;
}

/**
 * 响应拦截器:请求成功,返回2xx的code
 * @param {Object} response
 */
function responseInterceptorSuccess(responseData) {
  // 判断请求是否一个文件流
  if (typeof responseData !== 'object') {
    return Promise.resolve(responseData);
  }
  // 成功
  if (responseData.code === 0) {
    return Promise.resolve(responseData);
  }
  // 逻辑报错
  if (responseData.code === 1001) {
    handle1002(responseData);
    return Promise.reject(responseData);
  }
  //   最后统一处理
  // 消息提醒,比如element的 message.error()
  console.log(responseData);
  return Promise.reject(responseData);
}

/**
 * 响应拦截器:请求失败,即返回除了2xx外的code
 * @param {Object} response
 */
function responseInterceptorError(response) {
  const httpCode = response.status;
  const error = {
    type: 'http',
    code: httpCode,
    message: '请求出错',
  };
  if (!window.navigator.onLine) {
    // 网路错误
    handleOffline(error);
    return;
  }
  if (httpCode === 403) {
    handle403(error);
    return;
  }
  //   后面可以接上其他的处理逻辑

  //   最后统一处理
  // 消息提醒,比如element的 message.error()
  console.log(error);
}

五、前后效果

在这里,我用 for 循环发送 10 个请求,模拟上述适用场景。

js 复制代码
     async function a() {
        const res = await ajaxFunc({
          url: '/ok',
          type: 'get',
          data: {
            name: 'zhangsan',
            age: 18,
          },
        });
        console.log('业务代码--请求成功返回:', res);
      }

      for (let i = 0; i < 10; i++) {
        a();
      }

这样一来,你就能有效地缓存请求结果并取消重复请求,优化网络请求的性能。希望这篇文章对你有所帮助

相关推荐
燃先生._.26 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235241 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js