diff --git a/.codebuddy/plans/fix-skyline-render-error-and-login-loop_e6d774eb.md b/.codebuddy/plans/fix-skyline-render-error-and-login-loop_e6d774eb.md new file mode 100644 index 0000000..fb96658 --- /dev/null +++ b/.codebuddy/plans/fix-skyline-render-error-and-login-loop_e6d774eb.md @@ -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`,`` 全项目 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,规避首帧冲突 +``` \ No newline at end of file diff --git a/miniprogram/app.json b/miniprogram/app.json index b93e4a2..0461dc2 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -36,6 +36,7 @@ "navigationStyle": "custom", "backgroundColor": "#F2F2F7" }, + "renderer": "webview", "style": "v2", "sitemapLocation": "sitemap.json", "lazyCodeLoading": "requiredComponents" diff --git a/miniprogram/custom-tab-bar/index.ts b/miniprogram/custom-tab-bar/index.ts index 891cbbf..707ddbf 100644 --- a/miniprogram/custom-tab-bar/index.ts +++ b/miniprogram/custom-tab-bar/index.ts @@ -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() + }) }, }, diff --git a/miniprogram/pages/login/login.ts b/miniprogram/pages/login/login.ts index 06c6aee..c6747f0 100644 --- a/miniprogram/pages/login/login.ts +++ b/miniprogram/pages/login/login.ts @@ -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 })) }, diff --git a/miniprogram/pages/mall/mall.ts b/miniprogram/pages/mall/mall.ts index db33665..4d88b03 100644 --- a/miniprogram/pages/mall/mall.ts +++ b/miniprogram/pages/mall/mall.ts @@ -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, diff --git a/miniprogram/pages/profile/profile.ts b/miniprogram/pages/profile/profile.ts index 266178a..2a610a6 100644 --- a/miniprogram/pages/profile/profile.ts +++ b/miniprogram/pages/profile/profile.ts @@ -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 }) diff --git a/miniprogram/pages/wardrobe/wardrobe.ts b/miniprogram/pages/wardrobe/wardrobe.ts index 387ac41..c623558 100644 --- a/miniprogram/pages/wardrobe/wardrobe.ts +++ b/miniprogram/pages/wardrobe/wardrobe.ts @@ -87,7 +87,7 @@ Page({ }, fetchQuota(): Promise { - 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 }) diff --git a/miniprogram/utils/request.ts b/miniprogram/utils/request.ts index e89f44a..abeebb6 100644 --- a/miniprogram/utils/request.ts +++ b/miniprogram/utils/request.ts @@ -174,9 +174,10 @@ export function request(options: RequestOptions): Promise { doCall() return } - auth.clearTokens() - // 非关键请求(红点轮询等)静默失败,不打断用户当前页面 + // 非关键请求(红点轮询、额度展示等)静默失败: + // 只 reject,不动登录态、不跳转,避免把已登录用户悄悄踢成未登录 if (!silentAuth) { + auth.clearTokens() handleAuthExpired() if (showError) showToast('登录已过期,请重新登录') } diff --git a/project.private.config.json b/project.private.config.json index 5ce8257..56ee9d5 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -6,7 +6,7 @@ "urlCheck": false, "coverView": false, "lazyloadPlaceholderEnable": false, - "skylineRenderEnable": true, + "skylineRenderEnable": false, "preloadBackgroundData": false, "autoAudits": false, "useApiHook": true,