资讯动态

Axios拦截器深度解析:从原理到实战,统一处理请求与响应

发布时间:2026/8/15 6:53:51 来源:尧图企业网站定制
1. 从一个真实的开发场景说起最近在做一个前后端分离的管理后台项目前端用的是Vue 3 TypeScript后端API是标准的RESTful风格。开发到一半产品经理提了个需求所有用户操作比如点击按钮、提交表单都需要在页面上显示一个全局的Loading动画直到接口返回结果。同时后端要求所有请求都必须在请求头里带上用户的登录凭证一个JWT Token。这听起来是两个独立的需求但如果你一个接口一个接口地去手动添加headers和loading状态管理代码很快就会变得臃肿不堪维护起来简直是噩梦。这其实就是axios拦截器Interceptors最典型的用武之地。axios.interceptors.request.use和axios.interceptors.response.use这两个方法允许你在请求发出前和响应返回后统一地对请求和响应进行“拦截”处理。你可以把它们想象成高速公路上的收费站和检查站request拦截器是请求发出前的“收费站”你可以在这里给请求“挂载”一些东西比如Tokenresponse拦截器是响应抵达后的“检查站”你可以在这里对响应进行“安检”和“分流”比如统一处理错误、关闭Loading。很多新手朋友知道有这么个东西也照着网上的代码配了但往往知其然不知其所以然。比如为什么我的request拦截器里修改了config有时候不生效response拦截器的两个参数onFulfilled和onRejected到底该怎么用拦截器执行顺序是怎样的今天我就结合自己踩过的坑和项目中的实战经验把这套机制掰开揉碎了讲清楚并附上可直接“抄作业”的、覆盖常见场景的示例代码。2. 拦截器核心原理洋葱模型与Promise链在深入代码之前我们必须先理解axios拦截器背后的运行机制。这能帮你从根本上避免很多诡异的问题。axios的拦截器实现本质上是一个Promise链的巧妙运用。你可以把它想象成一个“洋葱模型”请求从中心你的业务代码发出穿过一层层的request拦截器“外皮”到达服务器响应再从服务器返回穿过一层层的response拦截器“外皮”最终回到你的业务代码。2.1 请求拦截器Request Interceptors的执行栈当你调用axios.interceptors.request.use时你实际上是在一个栈Stack里添加了一个处理函数。注意是栈这意味着后添加的拦截器会先执行LIFO后进先出。这个设计可能有点反直觉我们来看个例子// 假设我们按顺序添加两个请求拦截器 axios.interceptors.request.use(config { console.log(请求拦截器1); config.headers[X-Custom-1] First; return config; }); axios.interceptors.request.use(config { console.log(请求拦截器2); config.headers[X-Custom-2] Second; return config; }); // 当你发起一个请求时控制台会输出 // 请求拦截器2 // 请求拦截器1 // 请求被发出headers里同时有 X-Custom-1 和 X-Custom-2为什么是“后进先出”这是为了确保拦截器之间的依赖和覆盖关系更灵活。例如你先添加了一个通用的拦截器设置基础URL后来又添加了一个针对特定模块的拦截器修改URL。后添加的特定拦截器先执行它可能会基于某些条件决定是否要覆盖基础URL然后再由先添加的通用拦截器进行兜底处理。虽然在实际开发中我们通常按逻辑顺序添加但理解这个机制很重要。注意每个拦截器函数必须返回config对象或者一个Promise最终resolve config对象。如果你忘了return或者return了别的东西请求就会在这个拦截器处“断掉”永远不会发出去。这是新手最容易踩的坑之一。2.2 响应拦截器Response Interceptors的执行队列与请求拦截器相反响应拦截器使用的是队列Queue模型即先添加的拦截器先执行FIFO先进先出。这符合我们的直觉响应从服务器返回后应该先经过最早注册的、最通用的处理逻辑。axios.interceptors.response.use(response { console.log(响应拦截器1处理通用逻辑); return response; }, error { console.log(响应错误拦截器1); return Promise.reject(error); }); axios.interceptors.response.use(response { console.log(响应拦截器2处理业务逻辑); return response; }, error { console.log(响应错误拦截器2); return Promise.reject(error); }); // 当请求成功时输出顺序是 // 响应拦截器1处理通用逻辑 // 响应拦截器2处理业务逻辑 // 然后response才到达你的.then()回调 // 当请求失败时输出顺序是 // 响应错误拦截器1 // 响应错误拦截器2 // 然后error才到达你的.catch()回调响应拦截器的函数接收两个参数第一个是成功回调onFulfilled第二个是失败回调onRejected。关键点在于无论你在哪个拦截器里return Promise.reject(error)这个错误都会跳过后面所有拦截器的成功回调直接触发后面拦截器以及最终你的.catch的失败回调。2.3 完整的“请求-响应”生命周期结合来看一个请求的完整生命周期是这样的你的业务代码调用axios.get(/api/data)。触发请求拦截器栈从最后一个添加的拦截器开始依次向前执行。每个拦截器都可以修改config。使用最终的config发起真正的网络请求。服务器返回响应。触发响应拦截器队列从第一个添加的拦截器开始依次向后执行。每个拦截器都可以处理response或error。经过所有响应拦截器处理后的结果无论是成功的response还是被reject的error最终传递给你的业务代码中的.then()或.catch()。理解了这个“洋葱模型”和Promise链你就能明白为什么拦截器的顺序、以及return的值如此重要。3. axios.interceptors.request.use 实战详解axios.interceptors.request.use是你在请求发出前进行“加工”的地方。它的回调函数会接收到一个config对象这个对象包含了本次请求的所有配置信息比如url,method,headers,params,data等。你可以修改这个对象然后返回它。3.1 基础应用自动添加认证Token这是最常见的使用场景。我们不在每个请求里手动设置Authorization头而是在拦截器里统一处理。// 创建一个独立的axios实例是个好习惯避免污染全局配置 const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }); // 请求拦截器 service.interceptors.request.use( config { // 从本地存储如localStorage获取token const token localStorage.getItem(access_token); // 如果token存在则将其添加到请求头 if (token) { // Bearer Token是JWT的一种常见格式 config.headers[Authorization] Bearer ${token}; } // 必须返回config return config; }, error { // 请求配置出错非常罕见直接抛出错误 console.error(Request config error:, error); return Promise.reject(error); } );为什么要把axios.create()单独提出来在大型项目中你很可能需要对接多个不同域名或配置的后端服务。为每个服务创建一个独立的axios实例可以避免配置相互覆盖。比如给主业务API设置一个实例给文件上传服务设置另一个超时时间更长的实例。关于Token存储的安全考量 上面的例子用了localStorage它简单易用但存在XSS攻击风险。对于安全性要求极高的应用如金融、医疗可以考虑使用httpOnly的Cookie来存储Token前端完全无法通过JS读取从而免疫XSS。但这需要后端配合设置Cookie属性且前端在发送请求时浏览器会自动带上Cookie无需手动设置Authorization头。你需要根据项目实际的安全等级来做权衡。3.2 进阶应用动态修改请求数据与URL拦截器不只是加个Header那么简单你可以基于业务逻辑动态地改变请求。场景一根据环境变量切换API前缀在开发、测试、生产环境中你的API基础地址可能不同。service.interceptors.request.use(config { // 假设我们通过注入环境变量来区分 const env process.env.NODE_ENV; let apiPrefix /dev-api; // 默认开发环境 if (env test) { apiPrefix /test-api; } else if (env production) { apiPrefix /prod-api; } // 如果config.url不是以http开头我们认为是相对路径为其添加前缀 if (!config.url.startsWith(http)) { config.url apiPrefix config.url; } return config; });场景二对特定类型的请求体进行序列化比如你的后端要求所有POST请求的data必须是application/x-www-form-urlencoded格式而不是默认的JSON。import qs from qs; // 需要安装 qs 库 service.interceptors.request.use(config { if (config.method post || config.method put) { // 检查请求头如果是表单提交则序列化data if (config.headers[Content-Type] application/x-www-form-urlencoded) { config.data qs.stringify(config.data); } // 对于上传文件FormData千万不要做任何处理 if (config.data instanceof FormData) { // 删除自动设置的Content-Type让浏览器自动生成带boundary的格式 delete config.headers[Content-Type]; } } return config; });踩坑提示处理FormData时一定要小心。FormData对象有自己特殊的格式浏览器会自动设置正确的Content-Type如multipart/form-data; boundary...。如果你在拦截器里错误地修改了data或Content-Type会导致文件上传失败。一个常见的做法是判断config.data instanceof FormData如果是就跳过任何数据处理逻辑。3.3 请求拦截器的错误处理axios.interceptors.request.use的第二个参数是错误处理函数但它捕获的不是网络请求的错误而是请求配置阶段发生的同步错误。这种错误极少发生通常是你自己在拦截器里写了bug比如尝试读取一个未定义的变量。service.interceptors.request.use( config { // 主逻辑 const userInfo JSON.parse(localStorage.getItem(user)); // 如果存储的不是合法JSON这里会抛错 // ... 其他操作 return config; }, error { // 这里捕获的是上面 JSON.parse 抛出的同步错误 console.error([Request Interceptor Error], error); // 通常我们直接拒绝这个Promise让后续流程包括响应拦截器和业务代码的catch能处理 return Promise.reject(error); } );4. axios.interceptors.response.use 实战详解响应拦截器是处理服务器返回数据的核心场所。它的成功回调接收response对象失败回调接收error对象。4.1 基础应用统一处理响应数据与错误后端返回的数据通常包裹在一个固定的结构里比如{ code: 200, data: {...}, message: 成功 }。我们希望在业务代码里直接拿到data而不是每次都response.data.data。service.interceptors.response.use( response { // 请求成功HTTP状态码在2xx范围内 const res response.data; // axios默认把服务器返回的数据放在data字段里 // 假设我们的业务成功码是200 if (res.code 200) { // 直接返回核心数据这样业务层.then(res ...)里的res就是我们要的data return res.data; } else { // 业务逻辑错误比如参数错误(40001)、未授权(40003) // 这里统一提示并返回一个Rejected Promise中断后续成功流程 console.error(业务错误 [${res.code}]: ${res.message}); // 可以在这里触发全局的错误提示组件 // Message.error(res.message); return Promise.reject(new Error(res.message || Error)); } }, error { // 请求失败HTTP状态码不在2xx范围或网络错误 console.error(网络错误:, error); if (error.response) { // 请求已发出服务器有响应但状态码不是2xx const { status, data } error.response; switch (status) { case 401: console.error(未授权请重新登录); // 触发登出逻辑清空token跳转登录页 // router.push(/login); break; case 403: console.error(拒绝访问); break; case 404: console.error(请求的资源不存在); break; case 500: console.error(服务器内部错误); break; default: console.error(其他错误: ${status}); } // 如果后端在错误响应体里也提供了消息可以优先使用 const errMsg data?.message || error.message; // Message.error(errMsg); } else if (error.request) { // 请求已发出但没有收到响应网络断开、服务器宕机、请求超时 console.error(无响应:, error.request); // Message.error(网络连接异常请检查网络); } else { // 在设置请求时触发了一些错误比如配置写错了 console.error(请求配置错误:, error.message); } // 将错误继续向后抛业务代码的.catch()还能捕获到 return Promise.reject(error); } );这个拦截器做了几件关键事剥离响应外壳成功时直接返回res.data业务层用起来很清爽。统一业务错误处理即使HTTP状态码是200但业务码不是200也视为错误统一提示并reject。精细化HTTP错误处理根据不同的HTTP状态码401、404、500等执行不同的操作如跳转登录页。处理网络层错误处理了无响应error.request和配置错误error.message的情况。4.2 实现全局Loading状态管理文章开头提到的Loading需求就可以在响应拦截器中优雅实现。思路是在请求拦截器中开始Loading在响应拦截器中结束Loading。我们需要一个全局状态管理工具比如Vuex、Pinia甚至一个简单的全局变量来存储Loading状态。// 假设我们有一个全局的Loading状态管理器 let loadingCount 0; // 记录当前正在进行中的请求数量 const setLoading (isLoading) { // 这里可以触发Vue组件、Pinia store或任何UI框架的Loading状态 console.log(isLoading ? 显示Loading... : 隐藏Loading); // 例如在Vue3Element Plus中ElLoading.service({ fullscreen: true }) }; service.interceptors.request.use(config { // 检查请求配置中是否有自定义字段来跳过Loading if (!config.hideLoading) { loadingCount; if (loadingCount 1) { setLoading(true); // 第一个请求开始时显示Loading } } return config; }); service.interceptors.response.use( response { // 响应成功关闭对应请求的Loading const config response.config; if (!config.hideLoading) { loadingCount--; if (loadingCount 0) { setLoading(false); // 最后一个请求结束时隐藏Loading } } return response; }, error { // 响应失败同样需要关闭Loading const config error.config; if (config !config.hideLoading) { loadingCount--; if (loadingCount 0) { setLoading(false); } } return Promise.reject(error); } ); // 在业务代码中如果某个请求不想触发全局Loading可以这样 service.get(/api/some-data, { hideLoading: true });为什么用计数器loadingCount因为页面可能同时发起多个请求比如一个页面初始化时并行请求用户信息和菜单列表。使用计数器可以确保在所有并行请求都完成后才关闭Loading避免Loading闪烁一个请求结束就关但另一个还在请求中。4.3 实现Token失效自动刷新与重试这是一个高级但非常实用的场景。JWT Token通常有有效期过期后需要用户重新登录体验很差。我们可以利用响应拦截器实现检测到Token过期如401错误时自动调用刷新Token的接口获取新Token后自动重试刚才失败的请求用户无感知。let isRefreshing false; // 是否正在刷新Token let failedQueue []; // 存储刷新期间失败的请求队列 // 处理队列中失败的请求 const processQueue (error, token null) { failedQueue.forEach(prom { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue []; }; service.interceptors.response.use( response response, async error { const originalRequest error.config; // 如果是401错误并且不是刷新Token的请求本身尝试刷新Token if (error.response?.status 401 !originalRequest._retry originalRequest.url ! /auth/refresh) { if (isRefreshing) { // 如果正在刷新将当前失败的请求加入队列等待刷新完成后重试 return new Promise((resolve, reject) { failedQueue.push({ resolve, reject }); }).then(token { originalRequest.headers[Authorization] Bearer token; return service(originalRequest); // 用新的token重试原请求 }).catch(err { return Promise.reject(err); }); } originalRequest._retry true; // 标记这个请求已经重试过避免死循环 isRefreshing true; try { // 调用刷新Token的接口 const refreshToken localStorage.getItem(refresh_token); const { data } await axios.post(/auth/refresh, { refreshToken }); const newAccessToken data.access_token; // 存储新Token localStorage.setItem(access_token, newAccessToken); // 更新service实例的默认请求头后续新请求会自动使用新Token service.defaults.headers.common[Authorization] Bearer newAccessToken; // 更新当前失败请求的配置 originalRequest.headers[Authorization] Bearer newAccessToken; // 刷新成功处理队列中的请求 processQueue(null, newAccessToken); isRefreshing false; // 重试原来的请求 return service(originalRequest); } catch (refreshError) { // 刷新Token也失败了比如refresh_token也过期了 processQueue(refreshError, null); isRefreshing false; // 执行登出操作 console.error(Token刷新失败请重新登录); // router.push(/login); return Promise.reject(refreshError); } } // 如果不是401错误或者是不需要处理的请求直接抛出错误 return Promise.reject(error); } );这个方案有几个关键点防重复刷新通过isRefreshing标志位确保同一时间只有一个刷新请求。请求队列在刷新期间其他因此失败的401请求被暂存到failedQueue等拿到新Token后批量重试。避免死循环通过originalRequest._retry标记已重试的请求并排除刷新Token接口本身/auth/refresh防止刷新接口返回401时又触发刷新逻辑。更新全局配置刷新成功后不仅更新当前请求的Token也更新axios实例的默认请求头这样后续的新请求就直接用新Token了。5. 拦截器的注册、移除与执行顺序控制5.1 如何注册和移除拦截器axios.interceptors.request.use和axios.interceptors.response.use都会返回一个对应的拦截器ID数字。你可以用这个ID来移除这个拦截器。// 注册拦截器并保存ID const requestInterceptorId axios.interceptors.request.use(...); const responseInterceptorId axios.interceptors.response.use(...); // 在某个时机例如组件卸载、用户登出时移除拦截器 axios.interceptors.request.eject(requestInterceptorId); axios.interceptors.response.eject(responseInterceptorId);这个功能在单页应用SPA中非常有用。例如在一个需要特殊权限的模块里你临时添加了一个拦截器来添加特定的请求头。当用户离开这个模块时你应该移除这个临时拦截器以免影响其他模块的请求。5.2 多个拦截器之间的执行顺序与依赖如前所述请求拦截器是栈后进先出响应拦截器是队列先进先出。在复杂项目中你可能有多个拦截器它们之间可能有依赖关系。最佳实践按功能分层注册我建议按功能将拦截器分组并按照清晰的逻辑顺序注册// 第一层最基础、最通用的拦截器最先注册响应拦截器最后注册请求拦截器注意顺序 // 请求拦截器后添加的先执行所以基础的要先加 const service axios.create({...}); // 1. 日志拦截器最底层应该最先接触到请求和最后接触到响应 const logReqId service.interceptors.request.use(config { console.log([Request] ${config.method.toUpperCase()} ${config.url}, config); return config; }); const logResId service.interceptors.response.use( response { console.log([Response] ${response.config.url}, response); return response; }, error { console.error([Response Error] ${error.config?.url}, error); return Promise.reject(error); } ); // 2. 认证拦截器在日志之后处理因为Token信息可能敏感不适合日志全量打印 service.interceptors.request.use(config { const token getToken(); if (token) config.headers.Authorization Bearer ${token}; return config; }); // 3. 业务层拦截器最上层最后处理请求最先处理响应 // 例如处理Loading、统一错误提示等 service.interceptors.request.use(config { if (!config.hideLoading) showLoading(); return config; }); service.interceptors.response.use( response { if (!response.config.hideLoading) hideLoading(); // 处理业务码 if (response.data.code ! 200) { showError(response.data.message); return Promise.reject(new Error(response.data.message)); } return response.data.data; }, error { if (error.config !error.config.hideLoading) hideLoading(); handleHttpError(error); return Promise.reject(error); } );在这个例子中执行顺序是请求发出时业务Loading拦截器 → 认证拦截器 → 日志拦截器 → 网络。响应返回时日志拦截器 → 业务错误处理拦截器 → 你的.then()。这样分层逻辑清晰也便于调试和后期维护。6. 在主流框架中的集成与封装6.1 在Vue 3 (Composition API)中的封装在Vue 3项目中我们通常会在src/utils/request.js或.ts中创建并配置axios实例然后将其导出供整个项目使用。// src/utils/request.js import axios from axios; import { ElMessage, ElLoading } from element-plus; // 以Element Plus为例 import { getToken, setToken } from /utils/auth; // 封装的Token操作 import router from /router; // 创建axios实例 const service axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, timeout: 15000, }); // 请求拦截器 service.interceptors.request.use( config { // 显示Loading if (!config.hideLoading) { config.loadingInstance ElLoading.service({ fullscreen: true, lock: true }); } // 添加Token const token getToken(); if (token) { config.headers[Authorization] Bearer ${token}; } return config; }, error { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response { // 关闭Loading if (response.config.loadingInstance) { response.config.loadingInstance.close(); } const res response.data; // 业务状态码判断 if (res.code ! 200) { ElMessage.error(res.message || Error); // 如果是未登录或token过期触发登出 if (res.code 401) { // 清除token跳转登录页 setToken(); router.push(/login); } return Promise.reject(new Error(res.message || Error)); } else { return res.data; // 直接返回核心数据 } }, error { // 关闭Loading if (error.config error.config.loadingInstance) { error.config.loadingInstance.close(); } // HTTP状态码错误处理 ElMessage.error(error.message || 请求错误: ${error.response?.status}); if (error.response?.status 401) { setToken(); router.push(/login); } return Promise.reject(error); } ); export default service;然后在组件中直接引入使用script setup import { ref, onMounted } from vue; import request from /utils/request; const userList ref([]); const loading ref(false); const fetchUsers async () { loading.value true; try { // 直接拿到处理后的 data const data await request.get(/user/list); userList.value data; } catch (error) { console.error(获取用户列表失败, error); } finally { loading.value false; } }; onMounted(() { fetchUsers(); }); /script6.2 在React中的封装使用Hooks在React中我们可以封装一个自定义Hook比如useRequest来集成axios拦截器和状态管理。这里展示一个结合axios实例和useState、useEffect的简单封装思路。// src/hooks/useAxios.js import { useState, useEffect, useCallback } from react; import axios from axios; import { message } from antd; // 以Ant Design为例 // 创建并配置axios实例同Vue示例略 const service axios.create({...}); // ... 添加拦截器 ... const useAxios (config, immediate true) { const [data, setData] useState(null); const [loading, setLoading] useState(false); const [error, setError] useState(null); const execute useCallback(async (executeConfig) { setLoading(true); setError(null); try { const mergedConfig { ...config, ...executeConfig }; const response await service(mergedConfig); setData(response.data); // 注意这里拿到的是拦截器处理后的数据如res.data.data return response; } catch (err) { setError(err); message.error(err.message || 请求失败); throw err; } finally { setLoading(false); } }, [config]); // 依赖config useEffect(() { if (immediate) { execute(); } }, [execute, immediate]); return { data, loading, error, execute }; }; export default useAxios;在组件中使用import React from react; import useAxios from /hooks/useAxios; const UserList () { const { data: userList, loading, error, execute: fetchUsers } useAxios({ url: /user/list, method: get }, true); // immediate为true组件挂载即执行 if (loading) return div加载中.../div; if (error) return div加载失败: {error.message}/div; return ( div button onClick{fetchUsers}重新加载/button ul {userList?.map(user li key{user.id}{user.name}/li)} /ul /div ); }; export default UserList;这种封装将请求状态loading, data, error和axios实例的拦截器能力结合在了一起在React函数组件中非常实用。7. 常见问题排查与性能优化7.1 拦截器不生效检查执行顺序和返回值问题明明添加了拦截器但请求头没变或者Loading没触发。排查检查拦截器是否注册成功确保你的代码确实执行到了axios.interceptors.request.use这一行。在单页应用中确保这段代码在发起请求之前被执行。检查config的修改和返回在请求拦截器中你对config的修改必须生效并且一定要return config;。一个常见的错误是写了异步操作比如从localStorage异步获取Token但没有正确处理。检查多个拦截器的覆盖关系如果你有多个请求拦截器后注册的会先执行。如果后面的拦截器把前面设置的headers覆盖了或删除了也会导致不生效。使用浏览器开发者工具的“网络Network”选项卡查看实际发出的请求头是否正确。检查axios实例你是否使用了正确的axios实例如果你创建了多个实例axios.create()拦截器是分别注册在各个实例上的互不影响。确保你的业务代码调用的是你添加了拦截器的那个实例。7.2 内存泄漏别忘了移除临时拦截器在单页应用SPA中如果你在某个组件如onMounted里动态添加了拦截器一定要在组件销毁如onUnmounted时将其移除。否则当组件多次创建和销毁时拦截器会重复添加导致逻辑错乱和潜在的内存泄漏。script setup import { onMounted, onUnmounted } from vue; import service from /utils/request; onMounted(() { // 添加一个临时拦截器为这个模块的请求添加特定Header window.tempInterceptor service.interceptors.request.use(config { config.headers[X-Module-Name] UserManagement; return config; }); }); onUnmounted(() { // 组件销毁时移除这个临时拦截器 if (window.tempInterceptor) { service.interceptors.request.eject(window.tempInterceptor); } }); /script7.3 性能考量拦截器中的逻辑要轻量拦截器函数会在每个请求的生命周期中同步执行。因此务必保持拦截器内的逻辑简单、高效。避免在拦截器中执行重度的同步计算、复杂的循环或阻塞操作。反面例子// 糟糕每次请求都进行大量数据计算 service.interceptors.request.use(config { const hugeData JSON.parse(localStorage.getItem(hugeDataSet)); const processedData heavyCalculation(hugeData); // 耗时操作 config.headers[X-Processed] processedData; return config; });优化方案缓存结果如果数据不常变化计算一次后存起来。异步操作要谨慎拦截器支持返回Promise但异步操作如等待一个异步函数会延迟请求发出。如果必须异步确保它是必要的并且处理好竞态条件。条件执行通过config里的自定义参数如skipProcessing: true来控制某些请求是否进入复杂的拦截逻辑。7.4 调试技巧利用拦截器打印完整日志在开发阶段一个强大的日志拦截器能极大提升调试效率。它不仅记录请求和响应还可以记录耗时。// 开发环境才启用详细日志 if (process.env.NODE_ENV development) { service.interceptors.request.use(config { config.metadata { startTime: new Date() }; console.group(%c [Request] ${config.method.toUpperCase()} ${config.url}, color: blue; font-weight: bold); console.log(Params:, config.params); console.log(Data:, config.data); console.log(Headers:, config.headers); console.groupEnd(); return config; }); service.interceptors.response.use( response { const endTime new Date(); const duration endTime - response.config.metadata.startTime; console.group(%c [Response] ${response.config.url} (${duration}ms), color: green; font-weight: bold); console.log(Data:, response.data); console.log(Status:, response.status); console.groupEnd(); return response; }, error { const endTime new Date(); const duration endTime - (error.config?.metadata?.startTime || new Date()); console.group(%c [Response Error] ${error.config?.url} (${duration}ms), color: red; font-weight: bold); console.error(Error:, error); if (error.response) { console.log(Status:, error.response.status); console.log(Data:, error.response.data); } console.groupEnd(); return Promise.reject(error); } ); }这个拦截器会在浏览器控制台以可折叠的分组形式清晰地展示每个请求的详细信息、耗时和响应结果颜色区分使得成功和失败一目了然。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价