如何解决前端请求并发和请求覆盖?

前言

最近在开发一些大屏,涉及到比较多的数据来源,接口也没有做聚合,导致页面需要调很多接口来填充页面数据,那么就会有接口并发的问题出现

页面太多接口并发请求会出现什么问题?

  1. 服务器压力会变大:大量的并发请求会导致服务器的负载增加,从而影响服务器的性能和稳定性。
  2. 网络拥堵:一个域名最多有 6 个并发请求(不同浏览器可能限制不一样),超过 6 个并发请求会导致网络拥堵,从而影响页面的加载速度和用户体验。
  3. 响应延迟:由于服务器需要处理大量的并发请求,所以响应延迟会增加,从而影响页面的响应速度和用户体验。

解决方式也有很多,比如:

  1. 负载均衡:分发请求到多个服务器上
  2. 聚合接口:将多个接口合并成一个接口,减少接口的并发请求
  3. CDN 内容分发:通过不同的域名进行请求,从而突破浏览器单个域名并发请求限制

以上都是基于运维和后端的角度,那么前端如何解决呢?

并发请求

思路:设置并发请求限制,用队列存放请求,每次请求会先判断是否超出设置的最大并发请求数,当请求完成后,从队列中取出下一个请求,直到队列中的请求全部完成。

export class RequestQueue {
  constructor(concurrency = 6) {
    this.concurrency = concurrency; // 设置最大并发数,默认为6
    this.queue = []; // 存放请求的队列
    this.current = 0; // 当前正在执行的请求数
  }

  // 处理队列中的请求(出队)
  dequeue() {
    while (this.current < this.concurrency && this.queue.length) {
      this.current++;
      // 从队列中取出下一个请求并执行
      const requestPromiseFactory = this.queue.shift();
      requestPromiseFactory()
        .then(() => {
          // 成功的请求逻辑
        })
        .catch((error) => {
          // 失败
          console.log(error);
        })
        .finally(() => {
          this.current--;
          this.dequeue();
        });
    }
  }

  // 添加请求到队列中(入队)
  enqueue(requestPromiseFactory) {
    this.queue.push(requestPromiseFactory);
    this.dequeue();
  }
}

代码解释:

  1. 构造函数 (constructor):初始化了并发数 (concurrency)、请求队列 (queue) 和当前正在执行的请求数量 (current)。

  2. 入队方法 (enqueue):将请求添加到队列中,并立即调用 dequeue 方法开始处理队列。

  3. 出队方法 (dequeue):从队列中取出请求并执行。如果请求成功,执行成功逻辑;如果请求失败,捕获错误并记录。无论成功或失败,最终都会调用 finally 块来减少当前正在执行的请求数量,并继续处理下一个请求。

实际使用

const requestQueue = new RequestQueue(6); // 创建一个并发数为6的请求队列

// 模拟一个异步函数
sleep(fn) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(fn);
    }, 2000);
  });
},

// 生成测试请求
const queue = [...Array(20)].map((_, i) => () =>
  this.sleep(
    axios
      .get('/api/test' + i)
      .then(r => console.log(i, '成功'))
      .catch(e => console.log('失败', i))
  )
);

// 添加请求到队列中
for (let i = 0; i < queue.length; i++) {
  requestQueue.enqueue(queue[i]);
}

请求覆盖

场景:先后有A、B两个请求,A请求还未返回,B请求已经发起,并且B请求的结果比A先返回,那么A请求就会覆盖B请求的结果,正常要的结果是B的结果覆盖掉A请求的结果

可以用队列来维护请求的顺序,按照队列的顺序发起请求,但这有种“杀鸡用牛刀”的感觉,因为我们完全可以取消之前的请求,用最新的请求结果来赋值

可以通过以下方式解决请求覆盖的问题:

  1. 时序控制:定全局标识,比如数字,依次累加,每个请求响应中判断当前的标识是否 ≥ 全局标识,是则返回结果,否则不返回结果。

  2. 取消旧请求:发送新请求时判断是否有旧请求,有则取消旧请求,然后再发送新请求。

方法一:时序控制

let requestId = 0; // 全局标识

// 发送请求
function sendRequest() {
  const currentRequestId = ++requestId; // 递增全局标识

  // 发起请求
  axios.get('/api/data')
   .then(response => {
      // 判断当前请求是否是最新的请求(如果有新的请求那么requestId在新的请求会+1,比当前这个方法的curentRequestId的要大)
      if (currentRequestId >= requestId) {
        // 处理响应数据
        console.log(response.data);
      }
    })
   .catch(error => {
      // 处理错误
      console.error(error);
    });
}

方法二:取消旧请求

// 通过axios的cancelToken来取消请求

let cancelToken; // 取消请求的令牌

// 发送请求
function sendRequest() {
  // 取消旧请求
  if (cancelToken) {
    cancelToken.cancel();
  }

  // 创建新的取消请求的令牌
  cancelToken = axios.CancelToken.source();

  // 发起请求
  axios.get('/api/data', {
    cancelToken: cancelToken.token
  })
 .then(response => {
      // 处理响应数据
      console.log(response.data);
    })
 .catch(error => {
      // 处理错误
      console.error(error);
    });
}
// 自定义的取消请求函数
let lastCancel = null;
let cancelable = (req, callback) => {
  let cb = callback;
  req.then(res => {
    cb && cb(res);
  })
  let cancel = () => {
    cb = null;
  }
  return cancel;
}
let sendRequest() {
  lastCancel && lastCancel();
  lastCancel = cancelable(axios.get('/api/data'), res => {
    console.log(res);
  })
}

关于我
loading