bugfix:loginfiled

This commit is contained in:
2026-09-13 01:27:21 +08:00
parent f897e1a391
commit b1a664a768
9 changed files with 128 additions and 11 deletions
@@ -0,0 +1,110 @@
---
name: fix-skyline-render-error-and-login-loop
overview: 关闭 Skyline 渲染引擎消除「FLOW_INITIAL_CREATION / Expected updated data」渲染层报错,并修复首次登录后被踢回登录页且按钮卡死的三处缺陷(silentAuth 仍清 token、登录页礼包请求触发登出、loading 不复位)。
todos:
- id: disable-skyline
content: 关闭 Skyline:私配 skylineRenderEnable 改 false,app.json 加 renderer webview
status: completed
- id: fix-silent-auth
content: 修复 request.ts:silentAuth 请求 401 时不再清空 token
status: completed
- id: fix-login-loading
content: 修复 login.ts:礼包请求加 silentAuth,统一 loading 全分支复位
status: completed
dependencies:
- fix-silent-auth
- id: mark-quota-silent
content: 给 profile、wardrobe、mall 的 fetchQuota 加 silentAuth 静默降级
status: completed
dependencies:
- fix-silent-auth
- id: tabbar-nexttick
content: custom-tab-bar 首帧 setData 延到 nextTick 做加固
status: completed
dependencies:
- disable-skyline
- id: verify-build
content: 执行类型检查并通读改动链路,确认无报错与登录闭环正常
status: completed
dependencies:
- disable-skyline
- fix-login-loading
- mark-quota-silent
---
## 产品概述
微信小程序「衣橱」项目,需修复两类问题:调试器刷屏的渲染层内部错误,以及新用户首次登录后被踢回登录页、登录按钮彻底失效的阻断性 Bug。
## 核心功能(修复项)
- 关闭 Skyline 渲染引擎,消除 `expect FLOW_INITIAL_CREATION end but get FLOW_CREATE_NODE` 与 `Expected updated data but get first rendering data` 渲染层报错
- 修复 `silentAuth` 静默请求仍清空 token 的缺陷,避免非关键接口 401 把已登录用户变成未登录
- 修复登录页 `loading` 永不复位导致登录按钮点击失效的问题
- 登录礼包额度、各页额度展示等非关键请求标记为静默失败,401 时只降级不登出
- 加固自定义 tabBar:首帧 setData 延后,避免再次触发首帧渲染冲突
## 技术栈
微信小程序原生框架 + TypeScript(`useCompilerPlugins: typescript`),基础库 3.17.2,`miniprogramRoot: miniprogram/`,无测试框架,通过微信开发者工具编译调试。
## 实现思路
分两条独立链路:**渲染引擎回归 WebView**(配置层,零业务逻辑改动)与**登录态生命周期收敛**(请求层 + 页面层)。
1. **渲染报错:关闭 Skyline**
- `project.private.config.json` 的 `skylineRenderEnable: true` 覆盖了 `project.config.json` 的 `false`,本地编译实际走 Skyline。
- 已全量确认项目未使用任何 Skyline 专属能力(无 `worklet`、无 `scroll-view type="list"`、无 `share-element`/`snapshot`/`grid-view`/`page-meta`,`<navigation-bar>` 全项目 0 处使用),关闭无功能损失。
- 同时存在 `tabBar.custom: true`(自定义 tabBar)与 `lazyCodeLoading: requiredComponents`(按需注入),这是 Skyline 下首帧报错的典型组合。
- 方案:私配置 `false` 立即生效;`app.json` 根目录补 `"renderer": "webview"` 作为全局兜底,防止其他机器私配误开。改后需「清缓存 → 重新编译」。
2. **登录被踢回:静默请求不得清空 token**
- 现网代码 `request.ts` 在 401 分支对 `silentAuth` 请求**仍然执行 `auth.clearTokens()`**,只是不跳转,导致 token 被悄悄抹掉。
- 修复:把 `auth.clearTokens()` 移入 `if (!silentAuth)` 分支内,静默请求仅 reject,不动登录态。这是「登录成功却被清登录态」的根因。
3. **按钮点不动:loading 全分支复位**
- `login.ts onLogin()` 仅在失败路径复位 `loading`;`showWelcomeGift` 的 then/catch、`showBirthGuide()`、成功 `switchTab` 分支均不复位,故 `if (this.data.loading) return` 使按钮永久禁用。
- 修复:抽取 `finishLoading()`(或在 `.finally` 中复位),保证任何分支最终都会 `setData({ loading: false })`。
4. **非关键请求不触发登出**
- `/quota/info`(礼包额度、上传额度展示)属展示型接口,401 不应踢人。给登录页 `showWelcomeGift` 与 `profile/wardrobe/mall` 的 `fetchQuota` 加 `silentAuth: true`,失败仅降级。
## 实现要点(防回归)
- `redirectToLogin()` 的幂等锁与「已在登录页则跳过」判断保持不变,它是消除并发 `reLaunch`(`routeDone with a webviewId not found`)的关键,不得回退。
- 页面 `onShow` 的登录守卫(`!auth.isLoggedIn() → redirectToLogin()`)保持不变:修复后静默请求不再清 token,正常用户不会被误踢。
- `app.ts` 的 `setAuthExpiredHandler` 只负责清登录态,跳转仍由 `request.ts` 单一入口负责,维持单一职责。
- 配置改动需提醒用户重启开发者工具并清缓存;`checkInvalidKey: true` 下 `renderer` 为 app.json 合法全局字段,不会告警。
## 架构设计
```mermaid
flowchart TD
A[页面/组件发起请求] --> B{needAuth 且未登录?}
B -- 是 --> C[直接 reject 11001 不发请求]
B -- 否 --> D[wx.request]
D -- 401/11001/11002/11004 --> E[doRefresh 刷新 token]
E -- 成功 --> D
E -- 失败 --> F{silentAuth?}
F -- 是 --> G[仅 reject 保留登录态]
F -- 否 --> H[clearTokens + onAuthExpired + 幂等 redirectToLogin]
```
## 目录结构
```
zt-closet-wxmini-frontend/
├── project.private.config.json # [MODIFY] setting.skylineRenderEnable 改 false,关闭本地 Skyline 调试
├── miniprogram/
│ ├── app.json # [MODIFY] 根目录新增 "renderer": "webview",全局兜底渲染引擎
│ ├── utils/request.ts # [MODIFY] 401 分支把 auth.clearTokens() 移入 !silentAuth 内,静默请求保留登录态
│ ├── pages/login/login.ts # [MODIFY] 礼包请求加 silentAuth;新增统一 loading 复位,覆盖礼包/引导/跳转全分支
│ ├── pages/profile/profile.ts # [MODIFY] fetchQuota 加 silentAuth: true
│ ├── pages/wardrobe/wardrobe.ts # [MODIFY] fetchQuota 加 silentAuth: true
│ ├── pages/mall/mall.ts # [MODIFY] fetchQuota 加 silentAuth: true
│ └── custom-tab-bar/index.ts # [MODIFY] attached 内首次 setData 与 syncUnread 延到 wx.nextTick,规避首帧冲突
```
+1
View File
@@ -36,6 +36,7 @@
"navigationStyle": "custom",
"backgroundColor": "#F2F2F7"
},
"renderer": "webview",
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
+5 -3
View File
@@ -21,10 +21,12 @@ Component({
lifetimes: {
attached() {
// 先用缓存渲染红点;未登录时不再发请求(否则会批量 401 并触发重定向)
// 首帧渲染流程结束后再更新:先用缓存渲染红点,未登录时不再发请求(否则会批量 401 并触发重定向)
const cached = (wx.getStorageSync(STORAGE_KEYS.UNREAD) as number) || 0
this.setData({ unread: cached })
this.syncUnread()
wx.nextTick(() => {
this.setData({ unread: cached })
this.syncUnread()
})
},
},
+5 -2
View File
@@ -59,7 +59,9 @@ Page({
wx.switchTab({ url: '/pages/wardrobe/wardrobe' })
}
})
.catch(() => {
.catch(() => {})
.then(() => {
// 无论成功还是失败都复位:分支走向礼包/生日引导时若不复位,登录按钮会永久点不动
this.setData({ loading: false })
})
},
@@ -71,7 +73,8 @@ Page({
},
showWelcomeGift(userInfo: UserInfo) {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false })
// 额度只用于礼包展示:失败静默降级,绝不能触发登出/清 token
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => this.setData({ giftItems: this.buildGift(userInfo, res.quotas), giftVisible: true }))
.catch(() => this.setData({ giftItems: this.buildGift(userInfo, []), giftVisible: true }))
},
+1 -1
View File
@@ -76,7 +76,7 @@ Page({
},
fetchQuota() {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false })
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => {
const quotas = res.quotas.map((q) => ({
...q,
+1 -1
View File
@@ -72,7 +72,7 @@ Page({
},
fetchQuota() {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false })
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => {
const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
if (upload) this.setData({ uploadRemain: upload.remain })
+1 -1
View File
@@ -87,7 +87,7 @@ Page({
},
fetchQuota(): Promise<void> {
return request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false })
return request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => {
const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
if (upload) this.setData({ uploadRemain: upload.remain })
+3 -2
View File
@@ -174,9 +174,10 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
doCall()
return
}
auth.clearTokens()
// 非关键请求(红点轮询等)静默失败,不打断用户当前页面
// 非关键请求(红点轮询、额度展示等)静默失败:
// 只 reject,不动登录态、不跳转,避免把已登录用户悄悄踢成未登录
if (!silentAuth) {
auth.clearTokens()
handleAuthExpired()
if (showError) showToast('登录已过期,请重新登录')
}
+1 -1
View File
@@ -6,7 +6,7 @@
"urlCheck": false,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": true,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,