ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue中axios重复请求的终极解决方案:从交互层到全局拦截

Vue中axios重复请求的终极解决方案:从交互层到全局拦截 做前端三年多我在好几个 Vue 项目里都被重复请求坑过。一开始是用户手快连点提交按钮后台收到两条一模一样的数据后来是列表页筛选条件一变老请求还没回来新请求又发出去了页面数据被旧响应覆盖表格跳动、loading 乱闪。最夸张的一次一个数据大屏在轮询接口时没有做任何兜底每三秒发一次请求其中一两个接口响应超过五秒请求越积越多后端直接把这台服务的内存打爆了。从那以后我把“请求去重”当作 Vue 项目的标配能力不是在某个页面里加个标志位那么简单而是在 axios 层面统一处理。这篇文章我就围绕“解决 vue axios 重复请求”这件事把我在实际项目中用过的方案、踩过的坑、思考过的细节全部梳理一遍。如果你是刚接触 Vue 不久、或者已经写过一段时间但一直被重复请求困扰的同学这篇文章应该能给你一套可以直接落地的思路和代码。1. 先弄清楚重复请求的本质不是 bug是交互和网络的错位在我接触过的团队里很多人把重复请求看作一个很低级的问题觉得“不就是加个 loading 把按钮禁掉吗”。但实际排查一遍线上问题就会发现重复请求的来源比想象中复杂得多。1.1 哪些场景最容易触发重复请求我把真实项目里最常见的触发场景整理了一下你可以对照着自己的项目看看用户快速连点提交表单、支付、保存配置时用户习惯双击或者猛点按钮如果按钮没有立即禁用同一个接口会瞬间发出多次。路由切换但请求未取消列表页进入详情页或者用户在列表页快速切换查询条件上一次请求还没返回组件已经被销毁但请求依然在跑。返回后就可能出现“组件卸载后 setState”的告警或者旧数据覆盖新数据。组件的重复挂载比如一个弹窗被多次打开每次打开都会拉取详情接口如果弹窗的销毁和重建做得不干净上一个实例的请求还挂着。轮询和长连接场景数据大屏、订单状态轮询、消息中心轮询如果轮询内部没有处理好“上一次请求是否完成”就会产生请求堆积。并发组件共享同一接口页面里有多个组件同时依赖同一个基础数据接口比如登录用户信息、字典数据如果不做全局缓存或者去重进入页面的瞬间可能会有 3 到 5 个一模一样的请求同时飞出去。Axios 拦截器重试机制有些团队给 axios 封装了自动重试逻辑网络错误或者接口超时后自动重发一次。但如果重试逻辑判断不严谨线上偶发请求会翻倍。你发现没有这些场景背后其实都是同一个矛盾交互层的“用户操作速度”和网络层的“响应速度”不一致甚至不同组件之间的生命周期也不一致。用户点的是一下网络请求可能已经飞出去好几个组件销毁了请求却还在服务器执行。1.2 重复请求带来的实际危害说完场景再说危害。不是我在危言耸听重复请求带来的问题往往是链式的对用户的直接体验伤害。最典型的就是支付或提交订单场景用户点了一下没反应又点了一下结果产生了两个订单。这种错误用户一定会归因于“你们的系统有问题”信任感瞬间崩塌。另外列表页数据闪跳、loading 一直转用户也会觉得系统很“笨”。对后端服务的无谓压力。每一次重复请求都会占用一次后端连接、可能触发一次数据库查询。如果请求里面还包含文件上传、邮件发送或者短信验证码资源的浪费就更明显。大量并发重复请求甚至会把后端接口打挂别觉得夸张我上面提到的大屏项目就是这么挂的。对前端自身状态的污染。多个请求同时进行时后返回的响应不一定是最后发出的请求对应的数据这就会导致页面展示的数据状态错乱。尤其是在竞态条件下旧请求晚于新请求返回会把新数据覆盖掉。这是最隐蔽、最不好排查的问题。理解了重复请求的本质和危害我们再来看怎么解决。市面上处理重复请求的手段其实不少但从代码架构的角度看最合理的分层是能用交互层解决的就不要扩展全局能全局统一解决的就不要散落到每个页面里。2. 处理方案的分层设计从交互层兜底到全局拦截在我过手的项目里处理重复请求的方案五花八门。有人用一个isSubmitting布尔值就能搞定有人封装了上百行的 axios 拦截器。区别不在于谁更高级而在于场景不同。我把这些方案梳理成了四个层级你可以根据自己的项目形态来选择。2.1 交互层兜底按钮禁用是最简单也最容易被忽略的方案如果你的系统是后台管理系统交互以表单提交为主那么在“提交按钮”这个节点上做防重复点击通常就足够了。做法很简单在提交方法里加一个状态变量进入提交后立即置为 true结束后或异常后再置为 false配合按钮的 disabled 效果。// Vue 3 组合式风格示例 const submitting ref(false) async function handleSubmit() { if (submitting.value) return submitting.value true try { await submitApi(formData) message.success(提交成功) } finally { submitting.value false } }这段代码看起来简单但我想强调两个容易被忽略的点。第一finally而不是catch里恢复状态。很多人会把submitting.value false写在catch里这样一旦接口异常按钮就永远处于禁用状态用户只能刷新页面。用finally可以保证无论是成功还是失败按钮都会恢复。第二有些场景需要在handleSubmit刚开始就return而不是等await结束。上面的写法里第二次点击进入函数后会直接返回因为submitting.value已经是 true 了。这个就是所谓的前端“互斥锁”思想简单但有效。按钮禁用方案最直观、最容易理解也最适合那些内部管理系统里的低频操作。但它的缺点是只针对某个操作生效如果一个页面里有多个入口都能触发同一接口比如表格里有一行“详情”按钮同时顶部也有一个“查看详情”按钮你就得在多个地方维护这个状态一不小心就会漏。2.2 接口层去重在 axios 层统一拦截相同请求如果系统里存在大量列表查询、筛选、搜索之类的高频请求场景纯靠按钮禁用是扛不住的。因为用户切换筛选条件、翻页、点击搜索这些操作本身不能被禁用但同一个请求最好只发一次。这时候就需要在 axios 层做一个“请求去重”的机制。这个思路的核心是发请求之前先根据一些规则生成该请求的“指纹”如果指纹已经存在说明这个请求还在路上那么新发起的相同请求就不发给后端或者直接把上一次的响应结果返回。import axios from axios // 用一个对象记录进行中的请求 const pendingMap new Map() function getRequestKey(config) { const { method, url, params, data } config return [method, url, JSON.stringify(params || ), JSON.stringify(data || )].join() } axios.interceptors.request.use((config) { const key getRequestKey(config) if (pendingMap.has(key)) { // 如果存在相同的请求则取消当前请求 const cancel pendingMap.get(key) cancel.cancel(重复请求已取消) } else { const controller new AbortController() config.signal controller.signal pendingMap.set(key, controller) } return config }) axios.interceptors.response.use( (response) { const key getRequestKey(response.config) pendingMap.delete(key) return response }, (error) { const key getRequestKey(error.config) pendingMap.delete(key) return Promise.reject(error) } )这里我用的手段是AbortController它的signal可以传递给 axios从而中止一个还在进行中的 HTTP 请求。我把每个“进行中”的请求保存到pendingMap里key 由请求的 method、url、params 和 data 共同生成。当相同 key 的请求再次到达时直接取消前一个请求。为什么选择“取消前一个”而不是“丢弃后一个”这里我要重点解释一下。在很多场景下用户发起相同的重复请求往往是因为第一次请求太慢他想再触发一次。如果简单地把后一个请求丢弃用户的第二次操作会“没有反应”体验其实也不好。而取消前一个、保留后一个能保证最终后端只处理最后一次请求页面上拿到的是最新数据。这种策略也叫“以新替旧”。使用AbortController的好处是它是浏览器原生 API不需要额外依赖相比axios.CancelToken未来也更稳妥因为 axios 官方已经在文档里标记旧版 CancelToken 为 deprecated 了。不过上面这个拦截器方案只能防住“相同请求在并发期间重复发起”的串扰对于那种用户点击一次、但请求本身因为网络原因超时后自动重试的情况还需要额外处理。这类问题放到后面的“常见问题排查”里再讲。2.3 全局缓存去重同一个接口只请求一次所有人共享结果如果说接口层去重解决的是“同一时间点上的重复请求”那么全局缓存去重解决的是“同一时间段内的重复请求”。最典型的场景就是用户信息、字典数据、省份城市列表这类基础数据。一个页面上有五个组件都需要字典数据如果每个组件都自己在onMounted里拉一次接口进入这个页面就会打五次一模一样的请求。常规做法是封装一个带缓存的请求函数第一次调用发请求后续调用直接返回缓存结果。我习惯把它做在 axios 之外因为并不是所有接口都适合缓存只有特定接口需要这种能力。// 最简单的缓存请求封装 const cacheMap new Map() async function requestWithCache(config, cacheTime 5 * 60 * 1000) { const key getRequestKey(config) const cached cacheMap.get(key) if (cached Date.now() - cached.timestamp cacheTime) { return Promise.resolve(cached.data) } const response await axios.request(config) cacheMap.set(key, { data: response.data, timestamp: Date.now() }) return response.data }这个方案在我负责过的几个后台项目里都用到过特别适合几个场景拉取用户的基本信息、拉取系统字典、拉取省市区树、拉取租户配置。效果很明显接口文档上的请求量肉眼可见地下降。但是我必须提醒一句这个原生的 Map 缓存只适合用在纯前端需要短期复用、并且对数据实时性要求不高的场景。如果缓存的数据可能在服务端被修改比如管理员改了某个配置项希望用户下次刷新页面就能看到你需要设置一个合理的缓存过期时间或者提供手动清除缓存的方法。不要为了性能牺牲数据一致性。缓存和去重经常被放在一起讨论但它们其实解决的是两类问题。去重解决的是“同一时刻重复发”的问题缓存解决的是“一段时间内重复发”的问题。在大型系统里建议两者同时存在。2.4 业务幂等想让后端不做重复事得靠唯一标识前端做了拦截、做了缓存是不是就万事大吉了不是。前端层面的方案能防住“同一个用户、同一个浏览器”的重复操作但如果用户开了两个标签页或者用户在手机端和电脑端同时操作甚至网络层因为 TCP 重传导致请求重复送达前端就管不了了。这时候就需要后端接口本身具备幂等性。前端能做的是生成一个唯一请求 ID通常叫 requestId或者业务幂等键idempotency key在发起请求时带上它。后端根据这个键判断是否已经处理过该请求如果处理过就直接返回之前的结果避免重复写入数据。function generateRequestId() { if (crypto crypto.randomUUID) return crypto.randomUUID() return Date.now().toString(36) Math.random().toString(36).slice(2) } // 在请求拦截器里为写操作自动加上 requestId axios.interceptors.request.use((config) { if (config.method post || config.method put || config.method patch) { config.headers[X-Request-Id] generateRequestId() } return config })这里我特意只给写操作加 requestId。查询操作本身应该是天然幂等的但写操作新增、修改、提交需要后端配合去重否则前端无论如何兜底都不能避免极端情况下的重复数据。我在项目里的经验是后端同事通常很乐意接这样一个 header 字段因为他们在数据库层面做唯一约束或者缓存判断时也需要一个可靠的标识。前端主动提供 requestId是一种成本低但非常专业的前后端协作方式。3. 动手实现一套带取消、去重、缓存能力的 axios 封装方案看了一堆不如直接把代码撸出来。下面我分享一套我在真实项目中使用的 axios 封装它同时实现了请求指纹生成相同请求自动取消以新替旧可选的接口缓存响应错误统一处理请求完成后的队列清理这套代码基于 Vue 3 Axios 1.x但你完全可以改成 Vue 2 或者纯 JS 项目来用。3.1 整体目录结构和基础配置先来看 axios 模块的整体结构。我习惯在src/utils/request.js里放核心逻辑再把接口层单独放在src/api目录下。这样业务页面只依赖 api 模块不会直接接触 axios。// src/utils/request.js import axios from axios import { message } from ant-design-vue const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }) // 存放进行中的请求 Map const pendingMap new Map() // 存放接口缓存的 Map const cacheMap new Map() function getRequestKey(config) { const { method, url, params, data } config let dataKey if (data) { try { // 有些情况下 data 可能是 FormData 或 Blob这里做一层兼容 dataKey typeof data string ? data : JSON.stringify(data) } catch (e) { dataKey String(data) } } return [method.toUpperCase(), url, JSON.stringify(params || ), dataKey].join() } function removePendingRequest(key) { if (pendingMap.has(key)) { const controller pendingMap.get(key) controller.abort() pendingMap.delete(key) } } // 请求拦截器 service.interceptors.request.use((config) { const key getRequestKey(config) removePendingRequest(key) const controller new AbortController() config.signal controller.signal pendingMap.set(key, controller) return config }) // 响应拦截器 service.interceptors.response.use( (response) { const key getRequestKey(response.config) pendingMap.delete(key) const config response.config // 开启缓存则写入缓存 if (config.cache) { cacheMap.set(key, { data: response.data, timestamp: Date.now(), ttl: config.cacheTime || 5 * 60 * 1000 }) } return response.data }, (error) { if (error.config) { const key getRequestKey(error.config) pendingMap.delete(key) } if (axios.isCancel(error)) { // 被我们主动取消的请求不需要提示用户 // 直接抛一个特殊标记方便调用方捕获 return Promise.reject(new Error(REQUEST_CANCELED)) } message.error(error.response?.data?.message || 网络异常请稍后重试) return Promise.reject(error) } ) // 对外暴露 request 方法支持 cache 开关 export function request(config, options {}) { return service.request({ ...config, cache: options.cache, cacheTime: options.cacheTime }) } // 手动清空缓存 export function clearRequestCache(key) { if (key) { cacheMap.delete(key) } else { cacheMap.clear() } } // 取消全部进行中的请求常在路由切换或登出时调用 export function cancelAllPendingRequest() { for (const controller of pendingMap.values()) { controller.abort() } pendingMap.clear() } export default service这段代码我看到不少同学在使用时有个疑惑为什么在响应拦截器里要判断error.config而不是直接处理error原因是error可能是axios.isCancel(error)生成的取消错误也有可能来自网络层比如断网这两类错误不一定能拿到config对象。稳妥的写法是做一个存在性判断。还有一点需要注意getRequestKey在请求拦截器和响应拦截器里都必须使用完全一致的逻辑否则 map 里删除 key 时会找不到对应项导致 map 泄漏。我见过团队里有人在请求拦截器里用url method生成 key在响应拦截器里却用JSON.stringify(config)生成 key结果两个 key 对不上 pendingMap 越来越膨胀最后页面逐渐变卡。3.2 在项目里实际使用这套封装有了上面的 request 封装业务代码里调用接口时就清爽很多。比如一个典型的列表查询import { request } from /utils/request export function getOrderList(params) { return request({ url: /order/list, method: get, params }) } export function submitOrder(data) { return request({ url: /order/submit, method: post, data }) }在 Vue 组件里使用时如果某个查询接口希望短时间内的重复进入直接使用缓存可以直接开启 cacheconst dictData ref([]) async function loadDict() { const res await request({ url: /system/dict/status, method: get }, { cache: true, cacheTime: 10 * 60 * 1000 }) dictData.value res.data }同样如果某个页面在销毁时要取消当前页面的全部请求可以调用cancelAllPendingRequest()。需要注意这个方法是把全局所有请求全部取消所以通常只用在登出或者全局路由切换的场景。如果你只想取消某个组件的请求更合适的做法是在组件的onUnmounted里手动调用AbortController.abort()或者在请求函数里记录 signal不要一棒子打死全局。3.3 为什么这里用 AbortController 而不用 CancelToken关于用哪个取消方案我多说几句。axios 1.x 之前的项目里用CancelToken是很常见的写法axios 官方也提供了cancelToken参数。但我在新项目里已经全面切换到了AbortController原因有三个一是AbortController是浏览器的原生标准 API同样可以用于fetch如果你以后在项目里引入 fetch 或者混合使用多种请求库它的通用性更好。二是在 axios 的新版本里CancelToken 被标记为 deprecated虽然目前还能用但团队新代码没必要为一个未来可能移除的 API 买单。三是AbortController的使用方式更加直观你把信号传给请求请求还挂在大树上操作起来更符合直觉。当然如果你的项目锁死在 axios 0.x 版本没有办法升级那用 CancelToken 也是完全可行的。两种方案的思路一致核心都是在请求发出去时注册一个外部可调用的“中断句柄”在需要取消时调用它。3.4 路由切换时自动顺便取消无用请求在实际开发中“路由切换后老请求还在”的场景特别多。你可以在 Vue Router 的全局前置守卫里调用取消方法。但要注意不能所有页面都一刀切取消因为你可能同时有多个页面级的请求是希望保留的。更好的做法是只取消那些“可以被安全中断”的请求这也就是在请求配置上做一个标记。我在项目里的做法是给 axios 的扩展配置加一个cancelOnRouteChange字段默认为 true。如果是列表查询、详情查询这种请求路由切换后它自然不需要如果是上传文件这类不能中断的操作就显式置为 false。// 在请求拦截器里判断 config 的自定义字段 service.interceptors.request.use((config) { config.cancelOnRouteChange config.cancelOnRouteChange ! false // ... }) // 在路由守卫里 router.beforeEach((to, from, next) { // 取消所有可取消的请求 for (const [key, controller] of pendingMap) { if (controller.cancelOnRouteChange ! false) { controller.abort() pendingMap.delete(key) } } next() })这样你在业务代码里发起请求时默认就会被路由切换自动取消不用每个页面都去调cancelAllPendingRequest。只有极少数请求需要显式声明cancelOnRouteChange: false保留。4. 常见问题与排查技巧实录代码写完了不代表就万事大吉。在实际使用这套方案时我踩过不少坑也看过同事踩坑这里统一整理成一个速查表并把几个典型问题的排查思路展开说一下。问题现象可能原因解决思路点击按钮后请求发不出去按钮状态被finally漏恢复检查 submit 方法是否用了finally恢复状态重复请求拦截不生效key 生成逻辑不一致或没有在响应拦截器删除 key统一 key 生成函数并在成功和失败回调里都删除 key页面组件销毁后仍然有报错请求没有取消回调还在执行组件卸载时调用AbortController.abort()或用路由守卫自动取消Map 越来越大内存占用上涨请求 key 重复冲突或删除逻辑遗漏检查响应拦截器是否在每个分支都删除了 key旧请求覆盖新请求的数据并发请求竞态使用以新替旧策略取消旧请求或者用响应序号比较轮询请求堆积上一次请求未结束又发下一次在 setInterval 里判断是否已有进行中请求4.1 轮询接口的“上一次还没结束下一次又来了”这是我解决过的最典型问题。数据大屏项目里我需要每三秒拉取一次最新告警数据。最开始代码很简单setInterval(() { fetchAlertList() }, 3000)看起来规律的 3 秒一次但有个隐患如果接口响应时间超过 3 秒那么前一次的请求还没结束下一次的请求已经发出来了。请求量慢慢累积页面上的 loading 永远在转后端压力越来越大。后来我引入了一个简单的标志位let isRequesting false async function pollAlertList() { if (isRequesting) return isRequesting true try { const res await fetchAlertList() // 更新数据 } finally { isRequesting false } } setInterval(pollAlertList, 3000)这样不管接口响应多慢同一时间只会有一个轮询请求在路上。拿到响应后即使已经过了好多个周期也保留最新数据不会请求堆积。4.2 竞态导致的最终数据显示错误有一次我做搜索功能用户在搜索框里输入关键词是非常快的基本上每敲一个字就触发一次搜索请求。理论上最后展示的应该是最新的关键词对应的结果但实际中偶尔会出现“旧结果覆盖新结果”的情况。原因很简单第一个请求慢第二个请求反而快。结果第一个请求的响应最后到达覆盖了第二个请求的数据。单纯的“以新替旧”策略能解决一部分问题因为在新请求发出时会把旧请求取消掉。但如果网络层的取消并不可靠比如服务端已经处理完毕响应已经在路上了取消可能不会阻止回调执行。更稳妥的方案是使用一个递增的请求序号let searchSeq 0 async function handleSearch(keyword) { const currentSeq searchSeq const res await searchApi(keyword) if (currentSeq ! searchSeq) return // 说明已经不是最新的请求 renderList(res.data) }这个方案被称为“过期请求忽略法”它不是取消请求而是让旧请求的响应到达后直接丢弃。在 axios 层和 UI 层各做一层双保险我在实际项目里就是这么干的。4.3 运行环境中 AbortController 不可用怎么办如果你要兼容很老的浏览器比如 IE 11 或者早期的移动端 WebViewAbortController可能不存在。这种情况下你可以做一个简单的 polyfill 判断或者降级使用 CancelToken。const controller typeof AbortController ! undefined ? new AbortController() : null if (controller) { config.signal controller.signal }如果AbortController不存在这个请求就无法被主动取消但至少在大部分现代浏览器上能正常工作。对于内部管理系统一般不用担心这种浏览器兼容问题。4.4 怎么判断自己的拦截器是否真的生效排查这类问题我习惯打开浏览器的 Network 面板看同一个接口是否被发起多次。如果 Network 里已经出现了“canceled”标记说明拦截器生效了如果 Network 里同时出现多个相同请求说明 key 生成或删除逻辑可能有问题。还有一种情况请求被拦截器取消后Network 里会显示(canceled)但你依然能在控制台看到请求发出去了只不过没有完成实际不会到达后端。如果你希望调试更直观可以在请求拦截器里加一行console.log打印 key 和当前 pendingMap 的大小。4.5 重复请求与接口缓存同时开启时的注意事项有同学会问如果同一个接口同时开启了去重和缓存会怎样我建议这两个功能尽量不要同时作用于同一个请求。因为去重是在“请求发出去之前”拦截而缓存是在“请求返回之后”存数据。去重能防并发缓存能防短时间重复读。如果某个接口数据变化频繁不建议开启缓存否则用户看到的数据可能不是最新的。在我的封装里缓存是从响应成功回调里写入的。如果你不希望某个请求读缓存就不要在调用时传cache: true。如果接口偶尔需要刷新可以调用clearRequestCache(key)手动清掉缓存下次请求就会重新拉取。5. 踩坑与经验总结这些细节决定了方案能不能用最后这部分我不写“总结”而是分享一些我在实战中沉淀下来的心得。这些细节很难从官方文档里看到但往往决定了你的请求去重方案是锦上添花还是徒增复杂度。5.1 请求 key 的生成一定要足够精确key 的生成是整个去重方案的基石。如果 key 太宽泛比如只包含url那么同一个接口只要稍有不同的查询参数都会被误判为重复请求导致部分数据拉取失败。如果 key 太严格比如把data里的一个随机时间戳也包含进去那么即便请求语义相同也会被当成不同请求去重形同虚设。我在实际项目里的标准是method、url、params、data 全部参与生成 key并且参数在参与生成前要经过稳定的序列化。这里尤其注意JSON.stringify的顺序问题。如果你用对象字面量构造 params顺序通常是一致的但如果你在某种场景下用变量拼接顺序可能会变。稳妥做法是用一个稳定排序的序列化函数function stableStringify(obj) { if (obj null || typeof obj ! object) return JSON.stringify(obj) if (Array.isArray(obj)) return [${obj.map(stableStringify).join(,)}] const keys Object.keys(obj).sort() return {${keys.map((k) ${JSON.stringify(k)}:${stableStringify(obj[k])}).join(,)}} }5.2 不要过度拦截有些请求不应该被去重做技术方案的时候很容易犯一个毛病能力强了就到处用。请求去重并不是所有接口都适合开启。比如文件上传接口同一个文件可能会被用户有意识地重复提交用于覆盖比如验证码发送接口重复发送需要被后端限制但如果前端强制拦截用户收不到验证码时会误以为没点中体验更差。所以我不建议在 axios 实例的默认配置里全局开启强制去重而是把去重做成一个可选开关针对高频查询类的请求开启对写操作和上传类操作则明确关闭。用我上面封装的request(config, options)方式在调用处显式控制可读性和可维护性都更高。5.3 取消请求后记得处理的善后逻辑当你用 AbortController 取消一个请求后axios 会抛出一个CanceledError错误。如果你在响应拦截器里对所有错误统一提示“网络异常”用户会看到一条莫名其妙的报错。所以必须判断axios.isCancel(error)然后把这种情况静默处理掉或者在调用处单独捕获。在我项目里还有一个更精细的做法在取消请求时给页面上的 loading 状态一个明确的管理。如果一个组件同时发起多个请求其中一个被取消另外一个还在进行你不应该直接把整个 loading 关掉否则表格会闪一下。这个场景下需要用一个计数器来管理 loading 状态每发一个请求加一响应回来减一为 0 才关 loading。const loadingCount ref(0) function showLoading() { loadingCount.value } function hideLoading() { loadingCount.value Math.max(0, loadingCount.value - 1) if (loadingCount.value 0) { // 关闭 loading 动画 } }5.4 配合开发规范让重复请求问题从源头上减少工具只能兜底真正的解决之道还是规范。我所在的团队后来规定所有写操作提交前必须有提交中状态所有列表查询必须使用统一的 request 封装所有轮询必须带“进行中”判断。有了这些规范重复请求问题的发生率降低了八成以上。如果你的团队也有类似的需求可以在前端代码评审时检查几个点表单提交按钮是否加了 disabled列表查询是否使用了 axios 层去重轮询是否有请求堆积保护路由切换时是否取消了不必要的请求。这些东西看起来都很基础但很多时候线上事故就是这些“基础”没有做扎实。5.5 最后分享一个小技巧如果你正在做一个中大型应用建议把“请求去重”和“接口缓存”的状态做成可观测的。最简单的方式在getRequestKey函数里对同一个 key 命中多次时在 console 里打一条 warn这样你和其他同事在开发模式下就能直观地看到哪些接口存在重复请求。等上线前再把这些日志关掉成本极低但意义很大。我自己的经验是重复请求这个问题你说它小吧它确实不像性能优化、架构重构那么宏大你说它大吧它又直接关系到用户体验和后端稳定性。很多团队都是在线上出过一次事故之后才想起来要处理它。与其等着被坑一次不如花半天时间把 axios 层的地基打牢。上面这套方案是我在实际项目里反复调优后的结果你拿去用的时候根据自己项目的代码风格和团队规范调整一下应该能省掉不少排查的功夫。
返回列表