Files
zt-closet-wxmini-frontend/.codebuddy/plans/fix-401-relaunch-loop_32bcbbce.md

7.6 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
fix-401-relaunch-loop 修复新用户未登录时 /notification/unread 401 引发的并发 reLaunch 风暴(routeDone webviewId not found):请求层做登录守卫 + 登录失效跳转去重,custom-tab-bar 与页面侧停止无意义轮询与重复请求。
id content status
request-guard 改造 request.ts:未登录快速失败、401 收敛、新增 silentAuth 标记 completed
id content status dependencies
redirect-idempotent 在 app.ts 实现幂等登录失效跳转(单一入口+并发锁+已在登录页跳过) completed
request-guard
id content status dependencies
tabbar-unread 重构 custom-tab-bar:attached 只读缓存,新增 setUnread 与 syncUnread completed
request-guard
id content status dependencies
page-guards 为 notification、profile、wardrobe、login 页面加登录守卫并改用 setUnread completed
tabbar-unread
redirect-idempotent
id content status dependencies
verify 通读改动链路并做编译/类型检查,确认无 401 刷屏与重复 reLaunch completed
page-guards

用户诉求

新用户(无 token)一打开小程序就疯狂报错:

  • 控制台刷屏 GET https://zt-closet-backend.lzcwy.cn/api/v1/notification/unread 401 (Unauthorized)
  • 大量 [Page route 错误(system error)] routeDone with a webviewId 125 is not found

需要定位根因并修复,消除 401 刷屏与路由报错。

根因(已由代码核实)

  1. 未登录也发鉴权请求:custom-tab-bar/index.ts 的 attached() 无条件调用 refreshUnread() → fetchUnread(),无 auth.isLoggedIn() 守卫;request() 默认 auth: true,无 token 时服务端直接 401。
  2. 401 会触发全局跳转:request.ts 中 needAuth && (code 11001/11002/11004 || statusCode 401) → doRefresh()(无 refresh_token 直接失败)→ auth.clearTokens() + redirectToLogin() → wx.reLaunch('/pages/login/login')。showError: false 只压掉 toast,压不掉跳转,所以"静默轮询"也会触发 reLaunch。
  3. 并发放大:tabBar 有 5 个 tab 页,custom-tab-bar 每页一个实例,attached 各发一次;profile.onShow → syncTabBar → tabBar.refreshUnread() 再叠加;wardrobe.onShow、profile.onShow 还会各自发 /clothes、/quota/info、/user/info。多个请求同时 401 → 同时多次 wx.reLaunch,路由互相打断,导致 routeDone with a webviewId 125 is not found;且在登录页仍会继续 reLaunch。
  4. 重复请求链:notification.ts 的 fetchUnread() 成功后调用 tabBar.refreshUnread(),后者内部又 fetchUnread(),每次 onShow 多打一次接口。

核心改造目标

  • 未登录时不再发起鉴权请求,从源头消灭 401。
  • 登录失效跳转收敛为单一入口并做幂等去重(含"已在登录页则跳过"),杜绝并发 reLaunch。
  • 非关键轮询(未读红点)401 只失败不跳登录页。
  • custom-tab-bar 提供 setUnread(n),去掉 refreshUnread → fetchUnread 的二次请求。
  • tab 页 onShow 增加登录守卫,登录后进入页面才拉数据。

技术栈

微信小程序原生框架 + TypeScript(沿用现有 Page/Component 写法、现有 request.ts 请求层、auth.ts 登录态工具),不做框架引入或架构调整。

实现方案

采用「请求层收敛 + 调用点守卫」两层治理,最小改动面:

  1. 请求层(utils/request.ts)
  • 新增 silentAuth?: boolean(非关键请求,401 只 reject 不触发全局跳登录),用于未读红点等轮询。
  • auth: true 且 !auth.isLoggedIn() 时:直接 reject(new ApiError(11001, 'NOT_LOGIN')),不发请求、不跳登录页(避免未登录场景下的请求风暴)。
  • 401 分支只做 clearTokens() + reject,跳转统一交给 onAuthExpired 处理器(不再在 request.ts 内直接 wx.reLaunch)。
  • 保留 doRefresh() 单例锁逻辑不变(只处理"已登录但 token 过期"分支)。
  1. 全局跳转去重(app.ts)
  • 在 onLaunch 注入的 setAuthExpiredHandler 内做单一入口:
    • isRedirecting 布尔锁 + 时间窗(如 1000ms)内只执行一次;
    • 读取 getCurrentPages(),若栈顶已是 pages/login/login 则直接返回;
    • 执行 clearTokens() + clearUserInfo() + wx.reLaunch('/pages/login/login'),在 reLaunch 的 complete 回调里释放锁。
  • 这是消除 routeDone with a webviewId ... is not found 的关键(并发 reLaunch 互相打断 webview 生命周期)。
  1. custom-tab-bar(custom-tab-bar/index.ts)
  • attached() / refreshUnread() 只做「读缓存 zt_closet_unread → setData({ unread })」,不再自动发请求。
  • 新增 setUnread(n) 方法供外部(notification 页)直接更新红点,切断二次请求链。
  • 新增 syncUnread():仅在 auth.isLoggedIn() 时才请求 /notification/unread(silentAuth: true),供登录成功后/页面 onShow 主动调用。
  1. 调用点守卫
  • pages/notification/notification.ts:onShow 仅在已登录时 refresh() + fetchUnread();fetchUnread() 改为 silentAuth: true,成功后调用 tabBar.setUnread(count)(不再 refreshUnread())。
  • pages/wardrobe/wardrobe.ts、pages/profile/profile.ts:在 refresh() / fetchUser() / fetchQuota() 前加 auth.isLoggedIn() 守卫;profile.syncTabBar() 改为「已登录才 setUnread 缓存值 + 不触发请求」。
  • pages/login/login.ts:登录成功后 wx.setStorageSync('zt_closet_unread', 0),保证进入 tab 页时红点有初值。

实现要点(防回归)

  • request() 的 auth: false 请求(/user/wxlogin)不受任何改动影响,登录流程保持原样。
  • 「未登录直接 reject」不会弹出 toast:showError 默认 true 会提示,因此该分支应固定以 showError: false 语义处理,或显式不弹 toast,避免登录页被 toast 刷屏。
  • 跳转锁必须放在 app.ts 的 handler 里(唯一入口),不要在各页面重复实现,避免多套锁不一致。
  • 不改 app.json 页面顺序、不动 tabBar 配置,影响面仅 4~6 个文件。

架构关系

flowchart TD
  A[tab 页 onShow / custom-tab-bar attached] --> B{auth.isLoggedIn?}
  B -- 否 --> C[不发请求,红点用缓存]
  B -- 是 --> D[request 鉴权请求]
  D -- 401 --> E{silentAuth?}
  E -- 是 --> F[仅 reject,不跳转]
  E -- 否 --> G[clearTokens + onAuthExpired]
  G --> H{幂等锁 / 已在登录页?}
  H -- 是 --> I[跳过]
  H -- 否 --> J[reLaunch login(单例)]

目录结构

miniprogram/
├── utils/
│   ├── request.ts                     # [MODIFY] 未登录快速失败;401 收敛为 reject + onAuthExpired;新增 silentAuth 非关键请求标记
│   └── auth.ts                        # [只读参考] 复用 isLoggedIn/clearTokens/clearUserInfo
├── app.ts                             # [MODIFY] onAuthExpired 幂等跳转:单一入口 + 并发锁 + 已在登录页跳过
├── custom-tab-bar/
│   └── index.ts                       # [MODIFY] attached 只读缓存;新增 setUnread(n) 与 syncUnread()(仅登录态请求)
└── pages/
    ├── notification/notification.ts   # [MODIFY] 登录守卫;silentAuth 请求;成功后调用 tabBar.setUnread
    ├── profile/profile.ts             # [MODIFY] onShow 登录守卫;syncTabBar 不再触发请求
    └── wardrobe/wardrobe.ts           # [MODIFY] refresh 前加登录守卫
    └── login/login.ts                 # [MODIFY] 登录成功后初始化未读缓存为 0