这个问题是由服务器重定向 导致的页面生命周期并发问题。
为了解决 H5 缓存问题,服务器在进入页面后会在 URL 后追加时间戳并触发重定向。这导致浏览器在极短时间内触发了两次
onShow:一次是在初始 URL 下,一次是在带时间戳的 URL 下。由于重定向是浏览器的原生行为,它会打断当前的 JS 执行流。接口呈现canceled状态
常规的防抖机制依赖于内存状态,在重定向场景下会失效,因为第二次页面加载时,第一次的内存状态已经丢失。
我使用了
setTimeout将数据加载逻辑延迟了 180ms。这个方案的巧妙之处在于,它利用了重定向打断执行流的特性。当重定向发生时,第一次onShow中启动的宏任务实际上被丢弃或失效了。只有重定向稳定后的第二次onShow中的任务才会真正执行。同时,为了在重定向导致日志清空的情况下排查问题,我封装了一个基于
localStorage的追踪工具,记录了调用次数和触发时的 URL,从而精准定位到了重定向这个根因
1. 问题背景
在扫码进入微信 H5 支付页面,进入微信授权后重定向回来带code的地址时 需要用地址中的code调用接口authByCode换取openId,但是遇到了 authByCode 接口报错 code been used(code 已被使用)。这意味着同一个 code 在极短时间内被发起了两次请求。
2. 排查过程
2.1 现象
页面加载时报错,且由于页面发生了重定向(URL 变了),控制台日志被清空,无法直接看到报错堆栈,同时发现控制台用code换取openid的接口被调用了两次,第一次调用被取消,第二次调用提示code已经失效

2.2 初步假设与验证
假设 :uni-app 在 H5 端的 onShow 生命周期不稳定,可能被触发了多次。
验证 :在 onShow 中打印日志。
结果:发现日志确实打印了两次,但无法确定这两次触发的具体上下文(因为日志被清空了)。
2.3 引入工具追踪
为了解决日志丢失问题,编写了基于 localStorage 的持久化追踪工具 traceCount,不仅记录调用次数,还记录了触发时的 URL。(具体代码放在文章末尾)
javascript
// 工具函数核心逻辑
const countKey = `trace_count_${type}`;
const urlKey = `trace_url_${type}`;
// ... 记录 count 和 window.location.href ...
2.4 真相大白
服务器加入时间戳前后,浏览器地址如下

通过查看 localStorage 中的记录,发现了关键证据:
onShow确实被调用了两次。- 两次调用时的 URL 不同 :
- 第1次:
http://example.com/page - 第2次:
http://example.com/page?timestamp=1699999999
- 第1次:
结论 :问题根源找到了。之前为了解决 H5 缓存问题,在进入本页面后,服务器自动在 URL 追加时间戳并触发了重定向 。这导致浏览器在极短时间内加载了两次页面(虽然看起来是同一个),从而触发了两次 onShow。
3. 问题分析
3.1 并发冲突流程
-
T1 时刻:页面初次加载(URL 无时间戳)。
- 触发第1次
onShow。 - 执行
getOpenIdByCode。 - 发起请求 A :
authByCode?code=CODE_123。
- 触发第1次
-
T2 时刻:服务器响应重定向指令。
- 浏览器地址栏变为:
...?timestamp=xxx。
- 浏览器地址栏变为:
-
T3 时刻:页面在重定向后的 URL 下重新加载/激活。
- 触发第2次
onShow。 - 再次执行
getOpenIdByCode。 - 发起请求 B :
authByCode?code=CODE_123。
- 触发第2次
3.2 为什么报错?
请求 A 和请求 B 几乎同时发出。微信服务器先处理了 A,将 code 标记为失效。当请求 B 到达时,服务器检测到 code 已失效,返回 40029 invalid code。
4. 解决方案
4.1 为什么常规防抖失效?
虽然两次 onShow 是连续触发的,但它们是由重定向 这一浏览器原生行为隔开的。这导致它们处于不同的页面加载上下文中。
常规的防抖(如 lodash.debounce)通常依赖于内存中的变量状态。在重定向场景下,第一次页面的内存状态在第二次页面加载时已经清空(或者被视为全新的页面实例),导致防抖失效。
4.2 最终方案:setTimeout 延迟执行
这个例子中的用code换取openId接口在getOpenIdByCode方法中利用 JS 事件循环的机制,将数据加载逻辑推迟到下一个宏任务中。
javascript
async onShow() {
// ...
// 延迟 180ms 执行
setTimeout(() => {
this.getOpenIdByCode();
}, 180);
}
原理分析:
-
第1次
onShow(T1):- 启动定时器,将
getOpenIdByCode放入宏任务队列。 - 此时请求 A 尚未发出。
- 启动定时器,将
-
重定向发生 (T2):
- 浏览器开始处理重定向。
- 关键点 :重定向是一个同步的浏览器行为,它会打断当前的 JS 执行,并开始加载新的 URL。
-
第2次
onShow(T3):- 新页面加载,再次触发
onShow。 - 再次启动定时器,将
getOpenIdByCode放入宏任务队列。
- 新页面加载,再次触发
-
宏任务执行:
- 关键点 :由于重定向,第1次页面的宏任务队列实际上被丢弃 或打断了(或者即使执行了,它的上下文也即将失效)。
- 只有第2次页面的宏任务队列得以执行。
- 结果 :
getOpenIdByCode实际上只执行了一次(在重定向后的页面上)。 - 或者 :如果第一次请求 A 在重定向前极其幸运地发出了,180ms 的延迟也足以让请求 A 完成并缓存
openid。第二次执行时直接读取缓存,不再发请求。
修正后的结论 :setTimeout 的作用不仅仅是"错峰",更重要的是它利用了重定向打断当前执行流的特性 ,确保只有重定向稳定后的那次 onShow 才真正发起了有效的业务请求。
5. 总结与反思 (STAR原则)
Situation (情境)
在开发微信 H5 支付页面时,遇到了获取用户 openid 的接口报错 code been used 的问题。由于服务器为了解决缓存问题强制追加了时间戳并重定向,导致 onShow 生命周期被触发了两次。
Task (任务)
需要解决由页面重定向导致的 onShow 并发调用,进而引发的 authByCode 接口重复请求和 code 失效问题。
Action (行动)
- 工具开发 :编写了基于
localStorage的traceCount工具,解决了重定向导致控制台日志清空、无法排查的问题。 - 根因定位 :通过工具确认了
onShow触发了两次,且两次 URL 不同(第二次带时间戳),从而锁定了"服务器重定向"这一根本原因。 - 方案实施 :在
onShow中使用setTimeout延迟执行数据加载逻辑。利用 JS 事件循环和浏览器重定向打断执行流的特性,确保只有重定向稳定后的那次加载才真正发起请求。
Result (结果)
成功解决了接口并发报错问题。通过 setTimeout 配合重定向机制,有效避免了 code 的重复使用,支付功能恢复正常。同时,沉淀了 traceCount 这一通用排查工具,为后续解决类似的"日志丢失"问题提供了有力支持。
javascript
//使用:如traceCount('getOpenIdByCode');
export function traceCount (type, enableLog = true) {
if (!type) {
console.error('[traceCount] type is required');
return 0;
}
// 1. 生成唯一的 Key
const countKey = `trace_count_${type}`;
const timeKey = `trace_time_${type}`;
// 2. 获取当前计数
let count = parseInt(localStorage.getItem(countKey) || '0');
// 3. 计数 +1
count++;
// 4. 存回去
localStorage.setItem(countKey, count);
// 5. 记录时间戳
const now = Date.now();
localStorage.setItem(`${timeKey}_${count}`, now + window.location.href);
// 6. 打印日志 (可选)
if (enableLog) {
console.log(`[Trace] ${type} 第 ${count} 次调用`, new Date(now).toISOString() + window.location.href);
// 可选:打印调用栈
// console.trace(`[Trace Stack] ${type}`);
}
return count;
},
