返回

Axios拦截器计时不生效?原因分析与终极解决方案

vue.js

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 函数期望接收最多两个参数:第一个是处理成功响应的函数,第二个是处理错误的函数。上面代码里把两个函数 responseTimeInterceptorlogResponseTimeInterceptor (它本身还是个奇怪的包含两个函数的结构) 都传给了 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.configerror.config 访问到。

问题到底出在哪?剖析原因

理解 Axios 拦截器的工作流程是关键:

  1. 请求拦截器 (request.use) :

    • 在你代码发起请求之后,但在请求真正被发送到服务器 之前 执行。
    • 它接收请求配置 (config) 对象作为参数。
    • 你可以在这里修改 config,比如添加 headers、或者像我们现在需要的——记录一个起始时间戳。
    • 必须 返回 config 对象(或一个包含 config 的 Promise),否则请求就发不出去了。
  2. 响应拦截器 (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 里。

解决方案:一步到位,精准计时

明白了原理,正确的做法就很清晰了:

  1. 请求 拦截器中,获取当前时间,把它存到 config.meta (或者 config 下任何你喜欢的自定义属性) 里。
  2. 响应 拦截器中,定义好 成功失败 两种情况的处理函数。
  3. 在这两个处理函数里,都从传入的 response.configerror.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.logconsole.error。它会检查 config.meta.requestStartedAt 是否存在,避免因其他拦截器或配置问题导致拿不到时间戳而报错。
  • handleResponseSuccess: 这是响应拦截器中处理成功情况的函数。它接收 response 对象,调用 logExecutionTime 打印成功日志,然后必须返回 response
  • handleResponseError: 这是响应拦截器中处理失败情况的函数。它接收 error 对象。注意,并非所有错误都有 error.config(例如,请求设置阶段就出错),所以加了个判断。调用 logExecutionTime 打印失败日志,然后必须 throw error
  • responseTimeLogger: 把成功和失败的处理函数包装在一个对象里导出,方便后面直接在 response.use 中展开使用。你也可以选择分开导出 logSuccessResponseTimelogErrorResponseTime

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;

操作步骤:

  1. 将修正后的 logTimeInterceptor.js 代码放到你的项目对应路径下。
  2. 更新 httpClient.js 文件,确保:
    • logTimeInterceptor.js 正确导入了 recordStartTimeresponseTimeLogger (或者分开导出的 logSuccessResponseTimelogErrorResponseTime)。
    • 使用 httpClient.interceptors.request.use(recordStartTime) 来注册请求拦截器。
    • 使用 httpClient.interceptors.response.use(responseTimeLogger.onFulfilled, responseTimeLogger.onRejected) 来注册响应拦截器,分别传入成功和失败的处理函数。
  3. 运行你的应用,发起 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 请求耗时的日志记录了,而且代码结构清晰,易于维护。