接口请求如何合并?

访客 性能优化 2

本文目录导读:

  1. 为什么要合并请求?
  2. 常见合并场景与实现方案
  3. 后端配合(关键)
  4. 注意事项(避坑指南)
  5. 总结建议

接口请求合并(Request Batching / Request Merging)是一种前端性能优化策略,主要用于解决短时间内发起的多个相同或同类请求造成的资源浪费和服务器压力。

核心思想是:将多个独立的请求合并成一个请求发送,等后端返回一个合并后的响应后,再分发给各个发起方。

以下是几种常见的实现场景和方案:

为什么要合并请求?

  1. 减少HTTP连接数:HTTP/1.1有并发限制(通常6-8个),合并后可以避免排队阻塞。
  2. 减少网络往返:合并多个小数据包为一个大数据包,减少TCP握手和慢启动的开销。
  3. 降低服务器压力:减少数据库查询次数和连接池开销(例如将N个SQL合并为1个 IN 查询)。

常见合并场景与实现方案

场景1:短时间内的数据请求(最常见)

场景:页面中有10个不同的组件,每个组件在初始化时都需要获取用户信息,如果在100ms内触发了10次 /api/user/info,我们希望只发一次请求。

实现方案:请求去重 + 微任务调度(Microtask + Map)

使用一个 延迟队列,在一个微任务或短定时器(如 setTimeout(0))内收集所有请求,合并后一次性发送。

// utils/requestBatcher.js
class RequestBatcher {
  constructor() {
    this.pendingRequests = new Map(); // 用于去重和存储回调
    this.timer = null;
    this.batchWindow = 50; // 50ms 窗口期
  }
  // 返回一个 Promise
  fetch(url, params) {
    return new Promise((resolve, reject) => {
      // 生成唯一key:URL + 参数序列化
      const key = `${url}::${JSON.stringify(params)}`;
      // 如果已经有相同的请求在等待,只需添加回调,不用重复请求
      if (this.pendingRequests.has(key)) {
        this.pendingRequests.get(key).push({ resolve, reject });
      } else {
        this.pendingRequests.set(key, [{ resolve, reject }]);
      }
      // 清空之前的定时器并重新设置
      clearTimeout(this.timer);
      this.timer = setTimeout(() => {
        this.flush(); // 批量执行
      }, this.batchWindow);
    });
  }
  async flush() {
    // 拷贝当前队列并清空
    const requests = new Map(this.pendingRequests);
    this.pendingRequests.clear();
    this.timer = null;
    // 提取所有唯一的key
    const keys = Array.from(requests.keys());
    if (keys.length === 0) return;
    try {
      // 合并请求:这里假设后端提供了批量接口
      const response = await axios.post('/api/batch', {
        requests: keys.map(key => {
          const [url, paramStr] = key.split('::');
          return { url, params: JSON.parse(paramStr) };
        })
      });
      // 分发结果
      keys.forEach((key, index) => {
        const callbacks = requests.get(key);
        const result = response.data.results[index]; // 假设返回数组
        callbacks.forEach(cb => cb.resolve(result));
      });
    } catch (error) {
      // 分发错误
      keys.forEach(key => {
        const callbacks = requests.get(key);
        callbacks.forEach(cb => cb.reject(error));
      });
    }
  }
}
// 使用 (全局单例)
const batcher = new RequestBatcher();
// 组件A: batcher.fetch('/api/user', { id: 1 });
// 组件B: batcher.fetch('/api/user', { id: 1 }); // 会被去重
// 组件C: batcher.fetch('/api/user', { id: 2 }); // 同一个batch

场景2:自动收集式(GraphQL / 自定义封装)

如果你在后端能控制,还可以使用更“激进”的方案:将多个独立的REST请求自动转换为一个GraphQL请求

// 前端封装
const queryMap = {};
let queryId = 0;
let timer = null;
function collectQuery(query, variables) {
  return new Promise((resolve) => {
    const id = queryId++;
    queryMap[id] = { query, variables, resolve };
    clearTimeout(timer);
    timer = setTimeout(() => {
      const keys = Object.keys(queryMap);
      // 构建批量GraphQL请求
      const batchQuery = keys.map(k => {
        const { query, variables } = queryMap[k];
        return `${k}: ${query} at ${JSON.stringify(variables)}`;
      }).join('\n');
      fetch('/graphql', {
        method: 'POST',
        body: JSON.stringify({ query: `query { ${batchQuery} }` })
      }).then(res => res.json()).then(data => {
        keys.forEach(k => {
          queryMap[k].resolve(data.data[k]);
        });
      });
    }, 0); // 微任务/下个事件循环
  });
}

场景3:组件内部状态去重(React/Vue 等框架)

如果不想引入复杂的合并机制,最简单的优化是 “防抖 + 去重”

  • Vue:使用 computed + watch 合并高频触发。
  • React:使用 useMemo + useEffect,结合 Promise 的缓存。
// React Hooks 示例:请求缓存
const requestCache = useRef(new Map());
const fetchUser = async (userId) => {
  // 如果请求正在进行中,返回同一个 Promise
  if (requestCache.current.has(userId) && 
      requestCache.current.get(userId).status === 'pending') {
    return requestCache.current.get(userId).promise;
  }
  const promise = axios.get(`/api/user/${userId}`);
  requestCache.current.set(userId, { status: 'pending', promise });
  try {
    const result = await promise;
    requestCache.current.set(userId, { status: 'done', data: result });
    return result;
  } catch (error) {
    requestCache.current.delete(userId);
    throw error;
  }
};

后端配合(关键)

前端的合并请求,必须依赖后端的能力:

  1. 批量接口:后端提供一个 /api/batch 接口,接收一个请求数组 [{url, params, method}],返回一个响应数组。
  2. SQL IN 查询:例如前端传 ids: [1,2,3],后端执行 SELECT * FROM users WHERE id IN (1,2,3)
  3. GraphQL:天然支持在一条查询中请求多个不同实体。

注意事项(避坑指南)

  1. 请求时长不均匀:如果A请求需要10ms,B请求需要1000ms,合并后A必须等待B完成。解决方案:设置超时或使用“尽力合并”。
  2. 错误隔离:一个请求失败不应导致批次中所有请求失败,后端应返回精确的错误码。
  3. 非幂等请求(POST/PUT):合并时需谨慎,通常只建议合并 GET 请求查询类请求,写请求合并可能会导致数据冲突(同时更新一个字段)。
  4. 数据冲突:同一份数据在不同组件中可能已过期,如果使用了合并+缓存,需要确保一个组件更新后能有效触发重新获取。

总结建议

场景 推荐方案 复杂度
相同URL/参数 请求去重(共享同一个Promise) ⭐ 低
同类但不同参数(如不同id) 微任务调度 + 后端批量接口 ⭐⭐⭐ 中
不同URL(跨模块) GraphQL 或 微服务网关聚合 ⭐⭐⭐⭐ 高
后台/非关键数据 防抖合并 + 定时发送 ⭐⭐ 低

实际工程建议

  1. 优先使用 HTTP/2:它支持多路复用,能解决连接数瓶颈,在许多场景下避免了手动合并的复杂。
  2. 只在瓶颈处使用合并:不要对整个项目进行过度抽象,只对短时间内频繁触发数据量大的请求进行合并。
  3. 利用现有的库:如 axios 的 cancelTokenSWR/React Query 的 deduplication 功能,它们内置了请求去重。

标签: 接口请求合并

抱歉,评论功能暂时关闭!