7.6 KiB
7.6 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix-401-relaunch-loop | 修复新用户未登录时 /notification/unread 401 引发的并发 reLaunch 风暴(routeDone webviewId not found):请求层做登录守卫 + 登录失效跳转去重,custom-tab-bar 与页面侧停止无意义轮询与重复请求。 |
|
用户诉求
新用户(无 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 刷屏与路由报错。
根因(已由代码核实)
- 未登录也发鉴权请求:
custom-tab-bar/index.ts的attached()无条件调用refreshUnread() → fetchUnread(),无auth.isLoggedIn()守卫;request()默认auth: true,无 token 时服务端直接 401。 - 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。 - 并发放大: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。 - 重复请求链:
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 登录态工具),不做框架引入或架构调整。
实现方案
采用「请求层收敛 + 调用点守卫」两层治理,最小改动面:
- 请求层(
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 过期"分支)。
- 全局跳转去重(
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 生命周期)。
- 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 主动调用。
- 调用点守卫
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