Axios拦截器计时不生效?原因分析与终极解决方案
2025-04-12 09:30:35
Axios 响应时间拦截器不生效?原因分析与解决方案
写代码的时候,我们经常想知道 API 请求到底花了多少时间。用 Axios 拦截器来做这件事挺方便的:请求发出去前记个时,收到响应(或者报错)后再算一下时间差。听起来简单?但如果你尝试把记录开始时间和计算结束时间的逻辑分开写,再导入到 Axios 实例里,就可能碰到点麻烦——明明其他拦截器导入都没问题,偏偏这个计时相关的就不工作了。
就像下面这个例子,把计时逻辑拆分到 logTimeInterceptor.js
文件里:
// logTimeInterceptor.js (错误示范 V1)
export const responseTimeInterceptor = (response) => {
// 尝试在响应拦截器里记录开始时间
response.meta = response.meta || {}
response.meta.requestStartedAt = new Date().getTime()
return response
}
export const logResponseTimeInterceptor = ((response) => {
// 成功时打印时间
console.log(
`📡 API | Execution time for: ${response.config.url} - ${
new Date().getTime() - response.config.meta.requestStartedAt
} ms`
)
return response
},
// 失败时也打印时间
(error) => { // 注意:这里是 error 对象
// 尝试访问 response.config,但失败时可能不存在 response
// 并且,这个错误处理函数的写法也有问题
console.error(
`📡 API | Execution time for: ${error.config.url} - ${
new Date().getTime() - error.config.meta.requestStartedAt // 尝试从 error.config 读取
} ms`
)
throw error // 正确的错误处理应该抛出 error
}
)
然后在 httpClient.js
里导入使用:
// httpClient.js (错误示范 V1)
import axios from 'axios'
import { ApiSettings } from '@/api/config'
import {
responseTimeInterceptor,
logResponseTimeInterceptor // 这个导入和使用方式有问题
} from '@/api/interceptors/logTimeInterceptor'
const httpClient = axios.create(ApiSettings)
// ... 其他拦截器,比如 authInterceptor
// httpClient.interceptors.request.use(authInterceptor)
// 问题出现在这里
httpClient.interceptors.response.use(
responseTimeInterceptor, // 把记录开始时间的逻辑放在 response 拦截器
logResponseTimeInterceptor, // 并且这样传递两个参数给 use 是不对的
)
export default httpClient
这段代码的问题在于,它尝试在 响应 拦截器 (responseTimeInterceptor
) 里记录请求开始时间。这就好比你跑完步才想起来按秒表开始计时,肯定不对嘛!而且,response.use
函数期望接收最多两个参数:第一个是处理成功响应的函数,第二个是处理错误的函数。上面代码里把两个函数 responseTimeInterceptor
和 logResponseTimeInterceptor
(它本身还是个奇怪的包含两个函数的结构) 都传给了 response.use
的第一个参数位置,这语法就不对。
后来尝试修改,把记录开始时间的逻辑挪到 请求 拦截器里,把日志逻辑分成成功和失败两个独立的函数:
// httpClient.js (错误示范 V2)
httpClient.interceptors.request.use(responseTimeInterceptor) // 开始时间记录放到 request 拦截器
httpClient.interceptors.response.use(logResponseTimeInterceptor) // 成功日志
httpClient.interceptors.response.use(null, logResponseErrorTimeInterceptor) // 尝试单独注册错误日志 (注意第二个参数)
// logTimeInterceptor.js (错误示范 V2)
export const responseTimeInterceptor = (config) => { // 请求拦截器拿到的是 config 对象
// 这里尝试给 response 对象添加 meta,但请求拦截器里没有 response 对象!
// 正确的做法是把信息附加到 config 对象上
config.meta = config.meta || {} // 应该附加到 config
config.meta.requestStartedAt = new Date().getTime()
return config // 请求拦截器必须返回 config
}
export const logResponseTimeInterceptor = (response) => {
// 成功日志 - 读取 config.meta
console.log(
`📡 API | Execution time for: ${response.config.url} - ${
new Date().getTime() - response.config.meta.requestStartedAt // 读取 config 上的 meta
} ms`
)
return response
}
export const logResponseErrorTimeInterceptor = (error) => { // 错误拦截器拿到的是 error 对象
// 失败日志 - 读取 error.config.meta
console.error(
`📡 API | Execution time for: ${error.config.url} - ${
new Date().getTime() - error.config.meta.requestStartedAt // 从 error.config 读取
} ms`
)
throw error // 错误拦截器必须抛出 error 或返回一个 Promise.reject
}
这次方向对了——开始时间应该在请求拦截器里记。但是,responseTimeInterceptor
里的实现还是有问题:它试图操作 response.meta
,可请求拦截器根本就接触不到 response
对象,它操作的是 config
对象。时间戳应该附加到 config
对象上,因为它会一路传递下去,最后在响应拦截器 (response
) 或错误拦截器 (error
) 中可以通过 response.config
或 error.config
访问到。
问题到底出在哪?剖析原因
理解 Axios 拦截器的工作流程是关键:
-
请求拦截器 (
request.use
) :- 在你代码发起请求之后,但在请求真正被发送到服务器 之前 执行。
- 它接收请求配置 (
config
) 对象作为参数。 - 你可以在这里修改
config
,比如添加 headers、或者像我们现在需要的——记录一个起始时间戳。 - 必须 返回
config
对象(或一个包含config
的 Promise),否则请求就发不出去了。
-
响应拦截器 (
response.use
) :- 在收到服务器响应之后,但在你的
.then()
或.catch()
被调用 之前 执行。 - 它接收两个函数作为参数:
- 第一个函数 (
onFulfilled
):处理 HTTP 状态码在 2xx 范围内的成功响应。接收response
对象。必须 返回response
对象(或一个包含response
的 Promise),否则.then()
收不到数据。 - 第二个函数 (
onRejected
):处理 HTTP 状态码超出 2xx 范围的响应,以及网络错误等。接收error
对象。必须throw error
(或者返回一个Promise.reject(error)
),这样后续的.catch()
才能捕获到错误。你也可以在这里尝试恢复错误,返回一个正常的response
,但这在日志场景下不常用。
- 第一个函数 (
- 在收到服务器响应之后,但在你的
回看前面的错误尝试:
- 错误示范 V1 :主要错在① 把记录开始时间放到了响应拦截器里,太晚了;②
response.use
的参数传递方式不对,把多个函数挤在了一起,而不是按成功/失败分开传。 - 错误示范 V2 :虽然把记录时间挪到了请求拦截器,但实现错了,试图操作不存在的
response
对象,应该操作config
对象。虽然分离了成功和失败的日志函数,但注册错误处理时用了response.use(null, logResponseErrorTimeInterceptor)
的方式,虽然可行,但其实可以更简洁地放在同一个response.use
里。
解决方案:一步到位,精准计时
明白了原理,正确的做法就很清晰了:
- 在 请求 拦截器中,获取当前时间,把它存到
config.meta
(或者config
下任何你喜欢的自定义属性) 里。 - 在 响应 拦截器中,定义好 成功 和 失败 两种情况的处理函数。
- 在这两个处理函数里,都从传入的
response.config
或error.config
中取出之前存的开始时间,计算时间差并打印日志。
下面是修正后的代码:
1. 修正 logTimeInterceptor.js
把计时逻辑集中管理,导出清晰的函数。
// src/api/interceptors/logTimeInterceptor.js
/**
* 请求拦截器:记录请求开始时间
* 将开始时间戳附加到 config.meta 对象上
*/
export const recordStartTime = (config) => {
config.meta = config.meta || {}; // 确保 meta 对象存在
config.meta.requestStartedAt = Date.now(); // 使用 Date.now() 更简洁
return config; // 必须返回 config
};
/**
* 响应拦截器:处理响应(成功或失败),计算并打印请求耗时
*/
const logExecutionTime = (config, status) => {
const { url, meta } = config;
if (meta?.requestStartedAt) { // 确保我们记录过开始时间
const duration = Date.now() - meta.requestStartedAt;
const logFn = status === 'success' ? console.log : console.error;
logFn(`📡 API | [${status === 'success' ? 'OK' : 'FAIL'}] ${url} - ${duration} ms`);
// 可选:处理完后删除时间戳,避免内存泄漏(虽然影响微乎其微)
// delete meta.requestStartedAt;
}
};
/**
* 响应成功处理函数
* @param {AxiosResponse} response
*/
const handleResponseSuccess = (response) => {
logExecutionTime(response.config, 'success');
return response; // 必须返回 response
};
/**
* 响应失败处理函数
* @param {AxiosError} error
*/
const handleResponseError = (error) => {
// 网络错误或者请求被取消等情况下,error.config 可能不存在
if (error.config) {
logExecutionTime(error.config, 'error');
} else {
console.error('📡 API | Request failed without config info:', error.message);
}
throw error; // 必须抛出错误,以便后续 catch 处理
};
// 将成功和失败处理函数组合导出,方便在 response.use 中使用
export const responseTimeLogger = {
onFulfilled: handleResponseSuccess,
onRejected: handleResponseError,
};
// 如果你更喜欢分开导出,也可以这样做:
// export const logSuccessResponseTime = handleResponseSuccess;
// export const logErrorResponseTime = handleResponseError;
代码解释:
recordStartTime
: 这是请求拦截器。它拿到config
对象,给config.meta
(如果meta
不存在就先创建它)添加了一个requestStartedAt
属性,值为当前时间戳 (Date.now()
比new Date().getTime()
稍微简洁和可能略快一点点)。最后必须返回修改后的config
。logExecutionTime
: 一个辅助函数,用于统一打印日志的逻辑,根据状态(成功/失败)选择console.log
或console.error
。它会检查config.meta.requestStartedAt
是否存在,避免因其他拦截器或配置问题导致拿不到时间戳而报错。handleResponseSuccess
: 这是响应拦截器中处理成功情况的函数。它接收response
对象,调用logExecutionTime
打印成功日志,然后必须返回response
。handleResponseError
: 这是响应拦截器中处理失败情况的函数。它接收error
对象。注意,并非所有错误都有error.config
(例如,请求设置阶段就出错),所以加了个判断。调用logExecutionTime
打印失败日志,然后必须throw error
。responseTimeLogger
: 把成功和失败的处理函数包装在一个对象里导出,方便后面直接在response.use
中展开使用。你也可以选择分开导出logSuccessResponseTime
和logErrorResponseTime
。
2. 更新 httpClient.js
的拦截器设置
现在导入修正后的拦截器,并正确地应用它们。
// src/api/httpClient.js
import axios from 'axios';
import { ApiSettings } from '@/api/config';
import { recordStartTime, responseTimeLogger } from '@/api/interceptors/logTimeInterceptor';
// 假设你还有其他拦截器,比如处理认证的
// import { authInterceptor } from '@/api/interceptors/authInterceptor';
const httpClient = axios.create(ApiSettings);
// ============ 请求拦截器 ============
// 可以在这里添加其他请求拦截器,比如身份验证
// httpClient.interceptors.request.use(authInterceptor);
// 添加记录请求开始时间的拦截器
// 确保它在所有可能修改请求配置的拦截器之后,或者根据你的需求调整顺序
httpClient.interceptors.request.use(recordStartTime);
// ============ 响应拦截器 ============
// 添加记录并打印响应时间的拦截器 (包含成功和失败处理)
// responseTimeLogger 对象包含了 onFulfilled 和 onRejected
httpClient.interceptors.response.use(
responseTimeLogger.onFulfilled,
responseTimeLogger.onRejected
);
// 如果你之前是分开导出的:
// httpClient.interceptors.response.use(logSuccessResponseTime, logErrorResponseTime);
// 如果还有其他响应拦截器,比如统一处理数据格式或错误码转换
// httpClient.interceptors.response.use(handleDataFormat, handleErrorCode);
// 注意:拦截器的执行顺序是它们被 .use() 添加的顺序。
// 响应拦截器处理成功的链条是:服务器 -> A.onFulfilled -> B.onFulfilled -> .then()
// 响应拦截器处理失败的链条是:服务器 -> A.onRejected -> B.onRejected -> .catch()
// 如果某个 onRejected 处理了错误并且没有重新抛出,链条可能会中断。
export default httpClient;
操作步骤:
- 将修正后的
logTimeInterceptor.js
代码放到你的项目对应路径下。 - 更新
httpClient.js
文件,确保:- 从
logTimeInterceptor.js
正确导入了recordStartTime
和responseTimeLogger
(或者分开导出的logSuccessResponseTime
和logErrorResponseTime
)。 - 使用
httpClient.interceptors.request.use(recordStartTime)
来注册请求拦截器。 - 使用
httpClient.interceptors.response.use(responseTimeLogger.onFulfilled, responseTimeLogger.onRejected)
来注册响应拦截器,分别传入成功和失败的处理函数。
- 从
- 运行你的应用,发起 API 请求。现在你应该能在控制台看到类似
📡 API | [OK] /api/users - 123 ms
或📡 API | [FAIL] /api/posts/999 - 45 ms
这样的日志了。
深入一点:优化与注意事项
- 精度问题 :
Date.now()
提供的是毫秒级精度。对于绝大多数 API 耗时统计足够了。如果需要更高精度(比如纳秒级,虽然对网络请求意义不大),可以考虑使用performance.now()
。不过要注意performance.now()
返回的是相对于页面加载时间的毫秒数,计算差值没问题,但直接的值含义不同。 config.meta
的命名空间 :如果你有多个拦截器都需要在config
上附加信息,最好给meta
对象或者附加的属性起个独特的名字,或者使用更深层的嵌套,避免冲突。比如用config.meta._timing_start = Date.now()
或config.timing = { start: Date.now() }
。- 错误处理细节 :上面
handleResponseError
中加了对error.config
是否存在的检查。这是个好习惯,因为有些错误(比如请求被浏览器插件阻止,或网络层连接失败)可能发生在 Axios 能完整构造config
并附加信息之前。 - 拦截器顺序 :记住拦截器的执行是按照
use()
添加的顺序来的。请求拦截器是后加的先执行(像栈 LIFO),响应拦截器是先加的先执行(像队列 FIFO)。对于计时来说,recordStartTime
应该在请求真正发出前的最后阶段执行比较准确(通常是添加到request.use
的最后)。响应时间日志记录应该在对响应或错误做最终处理(比如数据转换或错误上报)之前执行,这样才能包含那些处理的时间(如果你想的话),或者放在它们之后,只计算纯粹的网络+服务器时间。这里我们把它放在了前面。
通过把记录开始时间的动作放在请求拦截器的 config
上,再在响应拦截器的成功与失败路径里分别读取 config
来计算差值,就能准确、可靠地实现 Axios 请求耗时的日志记录了,而且代码结构清晰,易于维护。