在网络请求的生命周期中,精确地统计请求耗时对于性能监控、瓶颈排查和用户体验优化至关重要。本文将介绍三种主流的实现方案,从简单直接到企业级实践,并提供经过优化的、可直接用于生产环境的优雅代码。
方案一:直接记录法(侵入式)
这是最直观的方法:在请求代码前后直接记录时间戳并计算差值。
import axios from 'axios';
async function directRequest() {
const startTime = Date.now();
try {
// 模拟一个耗时约 500ms 的 GET 请求
await axios.get('https://httpbin.org/delay/0.5');
const endTime = Date.now();
const totalTime = endTime - startTime;
console.log(`[方案一] 请求成功, 耗时: ${totalTime}ms`);
} catch (error) {
const endTime = Date.now();
const totalTime = endTime - startTime;
// 即使失败也应记录耗时,这对于问题排查同样重要
console.error(`[方案一] 请求失败, 耗时: ${totalTime}ms`, error);
}
}优点:
- 简单直接:代码逻辑一目了然,没有额外的封装概念。
缺点:
- 严重侵入业务代码:每个请求都需要重复添加计时逻辑,造成代码冗余。
- 难以统一管理:如果未来需要将耗时上报到监控平台或转换特定格式,需要修改所有请求点。
- 可维护性差:当项目中请求数量增多时,维护成本急剧上升。
适用场景:非常简单的、一次性的脚本或原型验证。
方案二:高阶函数封装(装饰器模式)
通过创建一个高阶函数(Higher-Order Function)来“包装”原始的请求函数。这个包装器负责添加计时逻辑,从而将核心业务与性能监控分离。
import axios from 'axios';
/**
* 包装请求函数以统计耗时
* @param {Function} requestFn - 原始的请求函数 (例如: axios.get)
* @returns {Function} - 包装后的函数
*/
function wrapRequest(requestFn) {
return async function (...args) {
const startTime = Date.now();
try {
const result = await requestFn(...args);
const endTime = Date.now();
console.log(`[方案二] 请求成功, 耗时: ${endTime - startTime}ms`);
return result;
} catch (error) {
const endTime = Date.now();
// 建议将耗时信息附加到error对象上,便于上层调用者捕获和分析
error.duration = endTime - startTime;
console.error(`[方案二] 请求失败, 耗时: ${endTime - startTime}ms`);
throw error; // 重新抛出错误,保持原始行为
}
};
}
// 使用
const wrappedGet = wrapRequest(axios.get);
const wrappedPost = wrapRequest(axios.post);
async function wrappedRequestExample() {
try {
await wrappedGet('https://httpbin.org/delay/0.5');
// await wrappedPost('https://httpbin.org/post', { data: 'test' });
} catch (error) {
// 此处仍能捕获到原始错误,如果需要耗时信息,可从error.duration获取
// console.log('捕获到错误,耗时:', error.duration);
}
}优点:
- 关注点分离:将计时逻辑与业务请求解耦,代码更清晰。
- 高复用性:一次封装,随处可用,轻松应用到任何请求函数上。
- 非侵入式:无需修改原始的业务请求代码。
缺点:
- 需要主动去“包装”每个请求函数,虽然比方案一好,但仍然需要一些手动操作。
适用场景:中小型项目,或不使用特定HTTP库(如Axios)的场景,追求简单灵活的解决方案。
方案三:HTTP库拦截器(企业级实践)
对于Axios这类成熟的HTTP库,其内置的拦截器(Interceptors) 是处理此类横切关注点(Cross-Cutting Concerns)的完美工具。它能实现全局、自动化的请求耗时统计。
import axios from 'axios';
// 1. 扩展 axios 配置类型
declare module 'axios' {
export interface InternalAxiosRequestConfig {
startTime?: number;
}
}
const service = axios.create({
baseURL: 'https://httpbin.org',
timeout: 5000,
})
// 2. 请求拦截器:记录开始时间
service.interceptors.request.use(
(config) => {
config.startTime = Date.now();
return config;
},
(error) => Promise.reject(error)
);
// 3. 响应拦截器: 计算耗时并处理成功/失败逻辑
service.interceptors.response.use(
(response) => {
const { config, status } = response;
const totalTime = Date.now() - (config.startTime ?? Date.now());
console.log(`[方案三] 响应成功 [${status}], 耗时: ${totalTime}ms`);
return response;
},
(error) => {
// 即使是网络错误或超时,只要请求发出去了,同样可以统计耗时
if (error.config) {
const totalTime = Date.now() - (error.config.startTime ?? Date.now());
console.error(`[方案三] 响应失败 [${error.response?.status || 'NETWORK_ERROR'}], 耗时: ${totalTime}ms`);
}
return Promise.reject(error);
}
);
// 使用:所有通过此 `service` 实例发起的请求都会自动统计耗时
async function interceptorRequestExample() {
try {
// 自动触发拦截器,无需额外代码
await service.get('/delay/0.5');
// 模拟一个失败请求
// await service.get('/status/500');
} catch (e) {
// 错误同样被拦截器处理过了
}
}优点:
- 完全自动化:一次配置,全局生效,所有请求(包括成功和失败)都会被自动统计。
- 零侵入业务代码:业务层完全无需关心计时逻辑。
- 功能强大:除了计时,拦截器还可用于统一处理认证、日志、数据格式化等。
缺点:
- 强依赖库:此方案与Axios等支持拦截器的库深度绑定。
- 配置相对复杂,需要对HTTP库有一定了解。
适用场景:使用Axios、Fetch(可通过类似fetch-wrapper实现)等库进行统一网络请求管理的项目,是企业级应用的最佳选择。
总结与选择
| 特性 | 方案一:直接记录 | 方案二:高阶函数封装 | 方案三:拦截器 |
|---|---|---|---|
| 侵入性 | 高,业务代码耦合度高 | 低,逻辑分离 | 无,完全分离 |
| 复用性 | 差 | 高 | 极高,全局生效 |
| 工作量 | 每个请求都需要 | 一次封装,多处调用 | 一次配置,全局生效 |
| 代码优雅度 | 低 | 中 | 高 |
| 适用场景 | 一次性脚本、原型 | 中小型项目、无统一HTTP库 | 大型项目、使用Axios等库 |
最终建议:
- 首选方案三(拦截器):如果你的项目已经使用了Axios,这是最优雅、最彻底的解决方案。
- 次选方案二(高阶函数):如果你的项目不依赖特定HTTP库,或者请求方式多样,这是一个非常灵活且干净的折中方案。
- 避免方案一:除非是极其简单的临时需求,否则应尽量避免使用这种方式,因为它会严重影响代码的长期可维护性。
版权属于:谁把年华错落成诗 所有,转载请注明出处!
本文链接:https://blog.pomears.com/archives/77.html
如果博客部分链接出现 404,请留言或者联系博主修复。