uni-app——uni-app请求封装踩坑:防重复请求导致页面卡死的终极解决方案

张开发
2026/4/11 0:12:31 15 分钟阅读

分享文章

uni-app——uni-app请求封装踩坑:防重复请求导致页面卡死的终极解决方案
一个看似完美的防重复请求方案却导致线上页面频繁卡死。排查过程让我对Promise有了更深的理解。一次诡异的线上事故上个月我们的小程序收到大量用户反馈页面突然点不动了。具体表现是点击任何按钮都没反应页面不报错也不崩溃只能靠微信自带的返回键退出重新进入后恢复正常最让人头疼的是这个问题无法稳定复现。测试同学点了一整天都没事用户却频频中招。直到有一天我在弱网环境下快速点击按钮终于抓到了现场——问题重现这是一个列表页用户点击卡片进入详情javascript// 业务代码简化版 const loadDetail async (id) { showLoading(加载中...); try { const data await api.getDetail(id); renderPage(data); } catch (error) { showToast(error.message); } finally { hideLoading(); // 关键关闭loading } };在弱网环境下快速点击同一个卡片3次第1次点击正常发起请求loading出现 ✅第2次点击什么都没有发生 ❓第3次点击什么都没有发生 ❓等第1次请求返回后loading消失了但页面再也点不动了。顺藤摸瓜问题出现在HTTP请求封装中。为了防止重复请求浪费资源我们加了一个缓存机制javascript// 问题代码 const pendingSet new Set(); const request (config) { return new Promise((resolve, reject) { const key ${config.url}_${JSON.stringify(config.data)}; // 检测到重复请求直接拦截 if (pendingSet.has(key)) { console.log(重复请求已拦截); return; // ❌ 问题在这里 } pendingSet.add(key); wx.request({ ...config, success: (res) resolve(res.data), fail: (err) reject(err), complete: () pendingSet.delete(key) }); }); };看起来没什么问题对吧Set记录进行中的请求重复就拦截完成就删除。但这段代码有一个致命的陷阱。Promise executor中的return陷阱很多人包括当时的我以为在Promise构造函数中return就等同于结束Promise。大错特错。让我们拆解一下javascriptconst promise new Promise((resolve, reject) { if (condition) { return; // 这只是从当前函数返回Promise没有任何变化 } resolve(data); }); // 这个promise会怎么样 // 答案是永远pending既不resolve也不reject当检测到重复请求时代码执行了return但Promise既没有resolve也没有reject。这个Promise就会永远处于pending状态。回到业务代码javascriptconst data await api.getDetail(id); // ↑ 如果这个Promise永远pending // 后面的代码永远不会执行 // finally块中的hideLoading()永远不会调用 // loading永远关不掉 // 页面被锁死这就是真相不是页面卡死而是loading遮罩层没有关闭遮住了所有点击事件。Promise状态流转图解text正常请求 ┌──────────┐ ┌──────────┐ ┌────────────┐ │ pending │ ──► │ resolved │ 或 │ rejected │ └──────────┘ └──────────┘ └────────────┘ finally ✓ finally ✓ 被拦截的请求问题代码 ┌──────────┐ │ pending │ ──► (永远停留在这里) └──────────┘ finally ✗ 永远不会执行正确的解决方案问题的核心是重复请求应该返回什么答案是返回同一个Promise让多个调用方共享同一个异步结果。javascript// 修复后的代码 const pendingMap new Map(); // 改用Map存储Promise const generateKey (config) { const method config.method || GET; const url config.url; const data config.data ? JSON.stringify(config.data) : ; return ${method}_${url}_${data}; }; const request (config) { const key generateKey(config); // 如果有正在进行的相同请求直接返回那个Promise if (pendingMap.has(key)) { console.log(复用已有请求); return pendingMap.get(key); // ✅ 返回Promise不是return; } // 创建新请求 const promise new Promise((resolve, reject) { wx.request({ ...config, success: (res) resolve(res.data), fail: (err) reject(err), complete: () { // 请求完成后清理缓存 pendingMap.delete(key); } }); }); // 缓存Promise pendingMap.set(key, promise); return promise; };核心改动只有一点从拦截返回空变成复用已有Promise。修复后的效果text第1次点击创建Promise1发送请求 第2次点击检测到重复返回Promise1同一个对象 第3次点击检测到重复返回Promise1还是同一个对象 请求返回后Promise1 resolve → 第1次、第2次、第3次调用的await同时收到结果 → 三个finally块都正常执行 → loading正确关闭为什么用Map而不是Set这是一个常见的误区javascript// Set只能存key拿不到Promise const set new Set(); set.add(key); // 想返回Promise拿不到 // Map可以存key-value对 const map new Map(); map.set(key, promise); const cached map.get(key); // 拿到了更完整的封装示例javascript// 完整的HTTP封装 class RequestManager { constructor() { this.pending new Map(); } generateKey(config) { const method (config.method || GET).toUpperCase(); const url config.url; const data config.data ? JSON.stringify(config.data) : ; return ${method}_${url}_${data}; } request(config) { const key this.generateKey(config); // 复用逻辑 if (this.pending.has(key)) { return this.pending.get(key); } // 发起新请求 const promise new Promise((resolve, reject) { const timeout config.timeout || 30000; let timer null; // 超时处理 if (timeout 0) { timer setTimeout(() { reject(new Error(请求超时)); this.pending.delete(key); }, timeout); } wx.request({ url: config.url, method: config.method || GET, data: config.data || {}, header: { Content-Type: application/json, ...config.header }, success: (res) { if (timer) clearTimeout(timer); if (res.statusCode 200) { resolve(res.data); } else { reject(new Error(HTTP ${res.statusCode})); } }, fail: (err) { if (timer) clearTimeout(timer); reject(new Error(err.errMsg || 网络错误)); }, complete: () { this.pending.delete(key); } }); }); this.pending.set(key, promise); return promise; } // 取消所有进行中的请求页面卸载时调用 cancelAll() { this.pending.clear(); } } export default new RequestManager();使用示例vuetemplate div classdetail-page button clickloadDetail(123) :disabledloading {{ loading ? 加载中... : 查看详情 }} /button div v-ifdetail classcontent h3{{ detail.title }}/h3 p{{ detail.content }}/p /div /div /template script setup import { ref } from vue; import request from /utils/request; const loading ref(false); const detail ref(null); const loadDetail async (id) { loading.value true; try { // 快速点击10次也只会有1个真实请求 // 其他9次复用同一个Promise const data await request.request({ url: /api/detail, data: { id } }); detail.value data; } catch (error) { console.error(error.message); } finally { loading.value false; // 永远会执行 } }; // 页面卸载时清理 onUnmounted(() { request.cancelAll(); }); /script这个陷阱比想象中更隐蔽很多人会犯类似的错误javascript// 错误1条件判断中return new Promise((resolve, reject) { if (!data) { return; // ❌ } resolve(data); }); // 错误2forEach中使用return new Promise((resolve, reject) { items.forEach(item { if (item.invalid) { return; // ❌ 只是退出forEach } }); resolve(); }); // 正确做法 new Promise((resolve, reject) { if (!data) { reject(new Error(data is required)); // ✅ return; } resolve(data); });记住在Promise构造函数中要么resolve要么reject没有第三条路。如何避免类似问题1. 代码审查检查点审查Promise相关代码时重点检查每个分支是否都有resolve或reject是否有单独的return;语句错误处理是否完整2. ESLint规则javascript// .eslintrc.js module.exports { rules: { // 禁止在Promise executor中直接return no-promise-executor-return: error, // 要求Promise reject必须使用Error对象 prefer-promise-reject-errors: error } };3. 单元测试javascriptdescribe(Request防重复测试, () { it(重复调用应该返回同一个Promise, async () { const p1 request({ url: /api/test }); const p2 request({ url: /api/test }); expect(p1).toBe(p2); // 同一个对象 const result await p1; expect(result).toBeDefined(); }); it(被复用的Promise应该正常resolve, async () { const results await Promise.all([ request({ url: /api/test }), request({ url: /api/test }), request({ url: /api/test }) ]); // 三个都应该正常返回 expect(results).toHaveLength(3); }); });写在最后这个问题让我深刻体会到看似正确的代码可能隐藏着致命的逻辑错误。防重复请求是一个常见的优化手段但实现时一定要注意被拦截的请求不能直接丢弃要复用原有PromisePromise executor中的return不是结束只是退出当前函数使用finally确保资源清理无论成功还是失败如果你也在项目中实现了类似的防重复请求功能不妨检查一下是否存在同样的问题。一个小问题可能卡死整个页面。一个return可能让用户骂娘。如果这篇文章对你有帮助欢迎点赞、收藏、转发。你的支持是我继续写作的动力

更多文章