Wenzi

如何合并同一接口的相同参数的请求

蚊子前端博客
发布于 2024/09/13 12:48
通过合并同一接口中具有相同参数的多次请求,减少服务器负载,提升响应效率,实现资源高效利用

这两天遇到一个场景,需要把中文的单位名称传给接口,获取对应的 id。但数据是从 excel 中读取的,有批量的数据需要处理,同时又基本都是同一个单位的,但偶尔可能会有其他单位的名称(比如该公司有子公司或者二级公司之类的)。

对于批量数据中都是同单位的名称,没必要每次都去请求接口,可以把相同公司的名字合并到一起,只发起一次接口请求即可。

解决方案的关键在于:如何把相同数据的请求缓存起来,然后在接口完成后触发。

在 JavaScript 中,我们可以利用Promise的特性:

只有 Promise 被 resolve() 的时候,才会执行 then()

这样,我们就可以利用 Promise 的特性,把相同参数的请求先放到数组中缓存起来,然后在接口完成后再触发。

1. 一个简单的例子 #

我们看个简单的例子:

let firstResolve = () => {};

const secondPromise = new Promise((resolve) => {
  firstResolve = resolve;
});
secondPromise.then(() => {
  console.log("secondPromise fullfilled");
});

setTimeout(() => {
  firstResolve();
}, 2000);

secondPromise 的then()方法什么时候执行,取决于 firstResolve() 的调用时机。

2. 再一个简单的例子 #

上面的例子很基础,可能还没太了解到多个请求如何得到响应。这里我们暂时不考虑参数是否相同的问题。

/**
 * 合并多个请求,apiCall表示请求的接口,需要返回一个Promise
 */
const mergeRequests = (apiCall) => {
  const queue = []; // 缓存多个请求

  return (...args) => {
    if (!queue.length) {
      // 还没有任何请求,那就发起请求
      const promise = apiCall(...args);

      // 待请求完毕后,执行所有创建的 Promise
      promise.then((response) => {
        queue.forEach((resolve) => resolve(response));
      });
    }

    /**
     * 已经有请求在执行了,那就先创建一个Promise(),
     * 这个 Promise 的`then()`什么时候执行,取决于 resolve() 的调用时机
     */
    return new Promise((resolve) => {
      queue.push(resolve);
    });
  };
};

执行:

const apiCall = (data) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("假装发起了一个请求", Date.now());
      resolve(`Response with ${JSON.stringify(data)}`);
    }, 600);
  });
};

const fn = mergeRequests(apiCall);

// 发起多个相同的请求
fn({ name: "蚊子的前端博客" }).then(console.log);
fn({ name: "蚊子的前端博客" }).then(console.log);
fn({ name: "蚊子的前端博客" }).then(console.log);

从调用结果中,我们可以看到,只有第一个请求发起了,后面的请求都返回了一个 Promise,当第一个请求完成后,会触发所有创建的 Promise 的then()方法。

3. 完整版 #

我们在上面已经可以合并请求了,但为了方便,没有区分参数,这里我们把参数的要素也加进来。

/**
 * 合并多个请求
 * @param {Promise} apiCall 表示请求的接口
 */
const mergeRequests = (apiCall) => {
  const requestQueue = new Map(); // 存储请求的队列,键是参数,值是 resolve 的数组

  return (...args) => {
    const key = JSON.stringify(args); // 使用参数作为键
    let promiseResolvers = requestQueue.get(key);

    if (!promiseResolvers) {
      // 如果没有队列,创建一个新的请求
      const promise = apiCall(...args);
      promiseResolvers = [];
      promise
        .then((response) => {
          promiseResolvers.forEach((resolve) => resolve(response));

          // 该参数对应的请求全部完成,清除key
          requestQueue.delete(key);
        })
        .catch(() => {
          // 请求失败,清除key
          requestQueue.delete(key);
        });
      requestQueue.set(key, promiseResolvers);
    }

    // 返回一个新的promise,它将在原始请求完成时解决
    return new Promise((resolve) => {
      promiseResolvers.push(resolve);
    });
  };
};

执行:

const apiCall = (data) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("假装发起了一个请求", data);
      resolve(`Response with ${JSON.stringify(data)}`);
    }, 700);
  });
};

const fn = mergeRequests(apiCall);

// 发起多个相同的请求
fn({ name: "蚊子的前端博客" }).then(console.log);
fn({ name: "蚊子的前端博客" }).then(console.log);
fn({ url: "https://www.xiabingbao.com" }).then(console.log);
fn({ name: "蚊子的前端博客" }).then(console.log);
fn({ url: "https://www.xiabingbao.com" }).then(console.log);
fn({ wechat: "前端小茶馆" }).then(console.log);
fn({ wechat: "前端小茶馆" }).then(console.log);

执行结果:

合并相同参数后的请求

从执行结果上可以看出,不同参数发起了不同的请求,而相同参数的请求,则只发起了一次。

标签:request
阅读(211)
Simple Empty
No data