h5微信授权code失效排查过程及解决记录

这个问题是由服务器重定向 导致的页面生命周期并发问题。

为了解决 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 中的记录,发现了关键证据:

  1. onShow 确实被调用了两次
  2. 两次调用时的 URL 不同
    • 第1次:http://example.com/page
    • 第2次:http://example.com/page?timestamp=1699999999

结论 :问题根源找到了。之前为了解决 H5 缓存问题,在进入本页面后,服务器自动在 URL 追加时间戳并触发了重定向 。这导致浏览器在极短时间内加载了两次页面(虽然看起来是同一个),从而触发了两次 onShow

3. 问题分析

3.1 并发冲突流程

  1. T1 时刻:页面初次加载(URL 无时间戳)。

    • 触发第1次 onShow
    • 执行 getOpenIdByCode
    • 发起请求 AauthByCode?code=CODE_123
  2. T2 时刻:服务器响应重定向指令。

    • 浏览器地址栏变为:...?timestamp=xxx
  3. T3 时刻:页面在重定向后的 URL 下重新加载/激活。

    • 触发第2次 onShow
    • 再次执行 getOpenIdByCode
    • 发起请求 BauthByCode?code=CODE_123

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. 第1次 onShow (T1)

    • 启动定时器,将 getOpenIdByCode放入宏任务队列。
    • 此时请求 A 尚未发出。
  2. 重定向发生 (T2)

    • 浏览器开始处理重定向。
    • 关键点 :重定向是一个同步的浏览器行为,它会打断当前的 JS 执行,并开始加载新的 URL。
  3. 第2次 onShow (T3)

    • 新页面加载,再次触发 onShow
    • 再次启动定时器,将 getOpenIdByCode放入宏任务队列。
  4. 宏任务执行

    • 关键点 :由于重定向,第1次页面的宏任务队列实际上被丢弃打断了(或者即使执行了,它的上下文也即将失效)。
    • 只有第2次页面的宏任务队列得以执行。
    • 结果getOpenIdByCode实际上只执行了一次(在重定向后的页面上)。
    • 或者 :如果第一次请求 A 在重定向前极其幸运地发出了,180ms 的延迟也足以让请求 A 完成并缓存 openid。第二次执行时直接读取缓存,不再发请求。

修正后的结论setTimeout 的作用不仅仅是"错峰",更重要的是它利用了重定向打断当前执行流的特性 ,确保只有重定向稳定后的那次 onShow 才真正发起了有效的业务请求。

5. 总结与反思 (STAR原则)

Situation (情境)

在开发微信 H5 支付页面时,遇到了获取用户 openid 的接口报错 code been used 的问题。由于服务器为了解决缓存问题强制追加了时间戳并重定向,导致 onShow 生命周期被触发了两次。

Task (任务)

需要解决由页面重定向导致的 onShow 并发调用,进而引发的 authByCode 接口重复请求和 code 失效问题。

Action (行动)

  1. 工具开发 :编写了基于 localStoragetraceCount 工具,解决了重定向导致控制台日志清空、无法排查的问题。
  2. 根因定位 :通过工具确认了 onShow 触发了两次,且两次 URL 不同(第二次带时间戳),从而锁定了"服务器重定向"这一根本原因。
  3. 方案实施 :在 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;
    },
相关推荐
天籁晴空2 小时前
微信小程序 静默登录 + 授权登录 双模式配合的设计方案
前端·微信小程序·uni-app
爱怪笑的小杰杰20 小时前
uni-app Vue3 国际化最佳实践:告别应用重启,优雅实现多语言切换
前端·vue.js·uni-app
yqcoder20 小时前
uni-app 数据缓存详解
缓存·uni-app
2501_9159214320 小时前
穿越HTTPS迷雾:Wireshark中的TLS抓包秘诀与文件合并方法
网络协议·ios·小程序·https·uni-app·wireshark·iphone
小徐_233321 小时前
uni-app 组件库 Wot UI 2.0 发布了,我们带来了这些改变!
前端·微信小程序·uni-app
yqcoder1 天前
uni-app 之 页面路由
uni-app
小离a_a1 天前
uniapp小程序添加路由全局限制,增加路由白名单,登录后接口跳转参数正常传递
小程序·uni-app
游九尘1 天前
uniapp获取定位uni.getLocation报错getLocation:fail maybe not obtain GPS Permission.
uni-app
雪芽蓝域zzs2 天前
uniapp 该应用与此设备的CPU不兼容
uni-app