文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结 投诉

INFO

最近上手公司Vue2老项目,由于没有统一的api请求管理,因此存在大量需要优化的重复请求。
网上大部分文章都是让其在axios的请求拦截器和相应拦截器中做文章,可我发现问题可能不是这么简单...

去重和缓存

1.取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
2.请求接口数据缓存:接口在设定时间内不会向后台获取数据,而是直接拿本地缓存
3.设定有效期限:在有效期限内限制请求发起,超出有效期后重新记录

Interceptors与adapter

虽然两者都用于扩展和自定义请求处理流程,但也存在一些差异:

特性adapterInterceptors
本质底层请求 / 响应处理器(负责实际发送请求) 中间件(用于预处理请求或响应数据)
核心作用实现跨环境请求(浏览器 / Node.js)、自定义底层通信逻辑(如模拟请求、缓存) 在请求发送前 / 响应接收后对数据进行统一处理(如添加认证头、错误处理)
控制范围可完全替换请求发送和响应解析的流程 仅能修改请求配置或响应数据,不影响底层通信方式
  • 从请求阶段执行顺序来看:
  1. 请求拦截器(修改请求配置,如添加 headers

  2. adapter(发送请求到服务器)

  3. 响应拦截器(处理服务器返回的响应数据,根据不同的响应码去做不同的操作)

javascript
axios.interceptors.request.use(() => console.log("请求拦截器"));

axios.create({ adapter: () => console.log("adapter") });

axios.interceptors.response.use(() => console.log("响应拦截器"));

axios.get("/api").then(() => {});

// 输出顺序:请求拦截器 → adapter → 响应拦截器

之所以写出两者对比,是因为网上大多数文章涉及到axios请求合并与缓存时通常会围绕请求拦截器响应拦截器展开。
在这其中不可避免地会使用Axios.CancelToken.source()进行请求取消操作,这种方式确实达到了去除重复请求的目的,但是也存在一些缺点:从逻辑上说却不够简洁优雅,浏览器中的Network中会看到大量红色警告(已取消的请求),在缓存请求方面是无法直接通过拦截器进行缓存,尤其是想缓存pending状态请求的话(这个还得看adapter =-=)。

根据上面的对比,我们可以知道拦截器是 “过滤器”:决定 “请求前后做什么”(如加 token、处理错误),是请求的中间处理层。而adapter执行者:决定 “如何发送请求”(如用 XHR 还是 Fetch),是请求的底层发动机。

缓存机制

先声明,关于缓存机制的定义是根据自己对axios结合实际业务的理解,并不是固定统一的标准,但确实符合大多数业务场景。
同源标记:相同请求方式、相同地址、相同请求参数
缓存机制分为两种:
1.有效期内缓存

在固定有效期内,一般为ms,同源标记的api请求一律视作同一组请求,首次请求已成功响应,二次请求时不超过有效期依然返回首次请求的Promise,也就说不存在响应后再次请求就不是缓存的情况,只要在有效期内一律返回首次缓存下的Promise,浏览器不会发起新的请求。

2.并发期内缓存

在固定有效期(也可理解为并发时间)内,一般为ms,同源标记的api请求一律视作同一组请求,首次请求若成功响应,则有效期重新计算;若首次请求还在响应中,则会缓存响应中的Promise,有效期内的后续请求一律返回首次缓存的Promise,也就说只要成功响应,则会让浏览器发起新的请求,反之若同源标记请求未响应时,不管有多少个重复请求都只会让浏览器发起一次请求。

上面两种方式相同之处在于同源标记的请求都只会请求一次,区别是对有效期的定义不同。

完整代码

javascript
import axios from "axios";
import { generateReqKey, longCacheEntityIds, longCacheUrls } from "@/api/common-lib";

const DEFAULT_CACHE_TTL = 5 * 1000;
const defaultAdapter = axios.defaults.adapter;
const pendingRequests = new Map();

export function adapter(config) {
  // 生成唯一的同源标记
  const requestKey = generateReqKey(config);

  // 记录当前时间
  const now = Date.now();

  // 设置请求参数过滤名单
  const cacheEntityIds = longCacheEntityIds.some((entityId) => {
    return requestKey.includes("/dataModel/queryDataModel") && requestKey.includes(entityId);
  });

  // 设置请求地址过滤名单
  const cacheUrls = longCacheUrls.some((url) => config.url.includes(url));

  // 根据条件决定是并发期内缓存还是有效期内缓存
  const shouldLongCache = cacheEntityIds || cacheUrls;

  let promiseCallback = null;

  // 如果已有相同请求正在进行中
  if (pendingRequests.has(requestKey)) {
    const { timestamp, promise, queue } = pendingRequests.get(requestKey);

    // 如果在有效期内,则合并请求
    if (now - timestamp <= DEFAULT_CACHE_TTL) {
      // 有效期内缓存则直接返回整个Promise
      // 并发期内缓存则需要将状态改变函数存入队列,等拿到首次请求响应结果时统一派发
      return shouldLongCache
        ? promise
        : new Promise((resolve, reject) => {
            queue.push({ resolve, reject });
          });
    } else {
      pendingRequests.delete(requestKey); // 过期则清理
    }
  }

  if (shouldLongCache) {
    promiseCallback = defaultAdapter(config);
  } else {
    promiseCallback = defaultAdapter(config)
      .then((response) => {
        // 和有效期缓存不同的是,并发缓存是在响应成功之后进行处理
        const { queue } = pendingRequests.get(requestKey) || {};
        if (Array.isArray(queue)) {
          // 所有状态改变函数共享同一个response
          queue.forEach(({ resolve }) => resolve(response));
        }
        pendingRequests.delete(requestKey);
        return response;
      })
      .catch((error) => {
        const { queue } = pendingRequests.get(requestKey) || {};
        if (Array.isArray(queue)) {
          queue.forEach(({ reject }) => reject(error));
        }
        pendingRequests.delete(requestKey);
        throw error;
      });
  }

  // 创建新请求队列并记录时间
  pendingRequests.set(requestKey, {
    timestamp: now,
    promise: promiseCallback,
    queue: [],
  });

  return promiseCallback;
}

使用时直接在axios实例化方法中声明:

javascript
import { adapter } from "@/api/adapter";

const service = axios.create({
  timeout: 50000,
  adapter,
});

效果对比

相同页面的请求在使用adapter优化之前和优化之后可以在Network中看到明显区别:
优化之前优化之前 优化之前,可以看见有19个请求,其中存在大量重复请求。
优化之后优化之后 优化之后,实际请求数降低近一般只有8个请求,资源消耗从75.8Kb降低至40.5Kb。