INFO
最近上手公司Vue2老项目,由于没有统一的api请求管理,因此存在大量需要优化的重复请求。
网上大部分文章都是让其在axios的请求拦截器和相应拦截器中做文章,可我发现问题可能不是这么简单...
去重和缓存
1.取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
2.请求接口数据缓存:接口在设定时间内不会向后台获取数据,而是直接拿本地缓存
3.设定有效期限:在有效期限内限制请求发起,超出有效期后重新记录
Interceptors与adapter
虽然两者都用于扩展和自定义请求处理流程,但也存在一些差异:
| 特性 | adapter | Interceptors |
|---|---|---|
| 本质 | 底层请求 / 响应处理器(负责实际发送请求) | 中间件(用于预处理请求或响应数据) |
| 核心作用 | 实现跨环境请求(浏览器 / Node.js)、自定义底层通信逻辑(如模拟请求、缓存) | 在请求发送前 / 响应接收后对数据进行统一处理(如添加认证头、错误处理) |
| 控制范围 | 可完全替换请求发送和响应解析的流程 | 仅能修改请求配置或响应数据,不影响底层通信方式 |
- 从请求阶段执行顺序来看:
请求拦截器(修改请求配置,如添加
headers)adapter(发送请求到服务器)响应拦截器(处理服务器返回的响应数据,根据不同的响应码去做不同的操作)
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,也就说只要成功响应,则会让浏览器发起新的请求,反之若同源标记请求未响应时,不管有多少个重复请求都只会让浏览器发起一次请求。
上面两种方式相同之处在于同源标记的请求都只会请求一次,区别是对有效期的定义不同。
完整代码
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实例化方法中声明:
import { adapter } from "@/api/adapter";
const service = axios.create({
timeout: 50000,
adapter,
});效果对比
相同页面的请求在使用adapter优化之前和优化之后可以在Network中看到明显区别:
优化之前 优化之前,可以看见有19个请求,其中存在大量重复请求。
优化之后 优化之后,实际请求数降低近一般只有8个请求,资源消耗从75.8Kb降低至40.5Kb。
