Skip to content

🐛 更新页与安装页补齐骨架屏与异步中间态,失败不再被渲染成成功 - #1721

Open
CodFrm wants to merge 3 commits into
fix/batchupdate-open-detailfrom
fix/batchupdate-loading-states
Open

🐛 更新页与安装页补齐骨架屏与异步中间态,失败不再被渲染成成功#1721
CodFrm wants to merge 3 commits into
fix/batchupdate-open-detailfrom
fix/batchupdate-loading-states

Conversation

@CodFrm

@CodFrm CodFrm commented Sep 3, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

更新页与安装页这条链路上有多处「异步动作没有中间态」和「失败被渲染成成功」的问题:

  • 取数失败被说成检查成功loadRecord 没有 catch,失败时记录仍是空数组,页面走到空态渲染「所有脚本均为最新(已检查 0 个脚本)」,唯一出口「重新检查」还会同样失败。
  • 点「检查更新」后的空窗期零反馈checking 只跟随 Service Worker 广播,往返回来之前按钮不禁用、可连点;服务端回的「正忙」与「结果够新已跳过」被直接丢弃,后者还会让 userCheckPendingRef 残留,在下一次后台检查完成时冒出一条用户没点过的 toast。
  • 忽略是 fire-and-forget:不写行状态也不看返回值(Service Worker 侧 IGNORE 分支根本没有返回值),点完行原地不动,用户会以为没点上而重复点。
  • 批量可并发:批量跑到一半仍可勾选并再次发起,两个循环同时推同一条进度,进度条来回跳、收尾出两条 toast;被「结果失效」中断时还会把已完成的条数一并抹掉。
  • 骨架与真实结构不对齐:桌面骨架缺工具条占位(数据到达时整表下移约 48px),移动骨架只画滚动区内的卡片,而真实态还会在滚动区外插入顶部选择栏与底部操作栏,上下同时挤压。
  • 安装页首屏说错话:状态屏把上下文 chip 写死成「脚本安装」,从更新页点进来必然先闪一次错的上下文;loading_desc 写「正在从来源下载」,但 uuid 入口的代码 Service Worker 早已写进 OPFS/TempStorage,根本不下载。这条入口最常见的失败是暂存条目被 30 分钟定时清理回收,而给出的出口是「重试」——重试多少次都是同一结果。
  • 安装页两个动作不置忙态toggleWatch / rejectExternalAccess 全程不置忙态,InstallActionsbusy 判据因此始终为假,连点会发两次安装 / 两次决定。

本次改动

三个提交按面拆分:

Service Worker(4b0afe6c

  • openUpdatePageByUUID / openUpdatePageboolean 改为 "opened" | "silent" | "failed"。命中静默更新时不开安装页却同样返回 true,页面无从区分,用户点完脚本名只看到转一圈、什么都没发生。
  • IGNORE 分支逐条回报结果。
  • checkScriptUpdate 的结果收敛成 TCheckScriptUpdateResult,并用 reason: "busy" 区分「已有检查在跑」与真正的失败。

安装页(a87c144b

  • 状态屏按来路分档:确知才渲染 chip,不猜;uuid 入口换掉「正在下载」的文案;补一条与就绪态操作栏等高的底部占位。
  • 暂存代码过期落专属终态,出口换成「重新检查更新」(请服务端重新备料)。
  • Monaco 就绪前渲染代码骨架,替代 340px 空白。
  • toggleWatch / rejectExternalAccess 补忙态,installsubmittingRef 重入守卫(UI 的 disabled 只作第一道防线——下拉菜单项在 phase 翻转前已展开时仍能被选中)。

批量更新页(dc399245

  • 取数失败落错误终态(等宽 detail + 重试 / 脚本列表)。
  • 主动检查由本地 pending 立刻接管忙态,并把服务端三条回执分别说出来;跳过时就地清掉待反馈标记。
  • 忽略复用与更新相同的行级阶段(working → success → 退场)。
  • 批量进行中互斥;被中断时保留已完成条数(BatchProgress.interrupted),「结果已失效」提示条加 role="alert" 与内联「重新检查」。
  • 骨架补齐工具条 / 顶部选择栏 / 底部操作栏占位并加 role="status" + aria-busy;空态下重新检查保留空态 + 顶部进度条,不再整页闪回骨架。
  • 脚本名的打开中状态改用 aria-disabled + onClick 早退,spinner 位改为常驻等宽空槽。

实现考虑

为什么忽略不走「结果已失效」。 初稿设计里,缓存随 Service Worker 回收后点忽略应提示「结果已失效」。回源核实后否掉了:scriptDAO.update 写的是脚本自身的 ignoreVersionsrc/app/repo/repo.ts:314),与 scriptUpdateCheck.cacheFull 无关——忽略一直是生效的,缺的只是回执和刷新广播。因此改成逐条回执:成功即由页面乐观收起该行,失败才停在失败态并保留重试;只有 UPDATE 才需要 record_expired,因为它真的依赖缓存里的 newCode

为什么用 aria-disabled 而不是 disabled 浏览器不向 disabled 表单控件派发指针事件,Radix Tooltip 靠 onPointerMove/onPointerLeave 开关,转圈期间「完整脚本名」tooltip 会失效——而这正是名字被截断、用户最想看全名的时刻;键盘用户按 Enter 后焦点还会掉到 <body>。防连点本来就由同步 ref 守卫承担,不依赖 disabled

为什么错误捕获放在 loadRecord 外面。 在 async 函数里 catch 到的 setState 无法被证明发生在 await 之后(被调用方同步抛出时它就是同步 setState),react-hooks/set-state-in-effect 会因此报错。改成在调用侧 .catch() 收口,promise 回调必然是异步的。

骨架的判据。 loading || (checking && empty && checktime === 0):首屏必然要骨架;此外只有「从未检查过」才用骨架,已经给出过空态之后再点检查,保留空态 + 顶部进度条即可,不要整页闪回骨架再闪回来。

已知限制

  • 批量汇总条仍会在 5 秒后自动收起,连带「查看已更新脚本」这个唯一出口一起消失。这与 fix/batchupdate-remove-autoclose 的改动主题重叠,留给那条分支处理。
  • 安装页加载屏仍没有超时兜底:Service Worker 唤醒失败且消息通道不 reject 时会永久转圈。需要在消息层加软超时,面比本 PR 大。
  • 未处理的零碎:安装页 keepAlive 定时器从不清理、远程图标无 onError 回退。

建议审查重点

  • runIgnoresrunUpdates 共用 rowStatesRef / scheduleRowExit,两者交叉时行状态是否仍然自洽。
  • checking: checking || pendingCheck 的合成忙态:pendingCheck 在服务端响应回来时解除,此时「检查完成」广播可能尚未到达。
  • Service Worker 侧 openUpdatePageByUUID 的三态返回是否覆盖了所有调用方(requestCheckUpdate 忽略返回值)。

关联

叠在 #1719 之上(base 选 fix/batchupdate-open-detail 而非 main,否则会把 #1719 的提交一并算进本 PR 的 diff)。#1719 合入后本 PR 的 base 需改回 main

验证

均在独占状态下执行(该套件对默认 850ms testTimeout 敏感,与其他任务并发跑会大量误超时):

  • npx vitest runTest Files 364 passed (364) / Tests 4556 passed (4556),退出码 0。
    基线 c73a876f 同样方式跑 → 364 passed / 4516 passed,即本 PR 净增 40 个用例、零回归。
  • npx eslint src/pages/batchupdate src/pages/install src/app/service/service_worker tests/mocks/CodeEditor.tsx → 0 error。
  • npx tsc --noEmit -p tsconfig.json → 0 error。
  • node scripts/check-i18n.mjs → 通过(新增 19 个 key 均已补齐 10 个语言包)。

未做浏览器实机验证;行为改动均由上述单元测试覆盖(新增用例含:取数失败落错误终态、busy/跳过/通道异常三条回执、忽略的行级阶段与逐条回执、批量互斥与中断保留计数、骨架占位与 aria-busyaria-disabled 不再是 disabled、静默更新回报 silent、安装页加载分档与过期终态、代码骨架、提交忙态与防重入)。

页面此前无从判断服务端到底做了什么:openUpdatePageByUUID 在命中静默更新时
不开安装页却同样返回 true,用户点完脚本名只看到转一圈、什么都没发生;
IGNORE 分支根本没有返回值,页面只能 fire-and-forget。

- openUpdatePageByUUID / openUpdatePage 返回 "opened" | "silent" | "failed"
- IGNORE 逐条回报结果。忽略写的是脚本自身的 ignoreVersion,与检查缓存无关,
  因此缓存随 Service Worker 回收后忽略照样生效,这里如实回报而不是谎报失效
- checkScriptUpdate 的结果收敛成 TCheckScriptUpdateResult 并用 reason 区分
  「已有检查在跑」与真正的失败,页面才能分别提示
从批量更新页点脚本名进来的必然是「更新」,加载屏却把上下文 chip 写死成
「脚本安装」,几百毫秒后再闪成「脚本更新」;描述写着「正在从来源下载」,
但这条入口的代码 Service Worker 早已备好,根本不下载。

- 状态屏按来路分档,未确知场景不渲染 chip(不猜),并补一条与就绪态操作栏
  等高的底部占位,避免就绪瞬间内容区高度再跳一次
- 暂存代码被定时清理回收时落到专属终态,出口换成「重新检查更新」——
  原来的「重试」在这个最常见的失败原因下重试多少次都是同一结果
- Monaco 实例就绪前渲染代码骨架,替代此前 340px 的纯空白
- toggleWatch / rejectExternalAccess 补忙态,install 加重入守卫:
  这两个动作全程不置忙态,连点会发出两次安装/两次决定
取数失败时记录仍是空的,页面直接走到空态,把一次加载失败渲染成
「所有脚本均为最新(已检查 0 个脚本)」这条与事实相反的成功终态;
点「检查更新」到服务端广播回来之间页面完全静止,期间可以连点。

- 取数失败落错误终态:等宽 detail 框 + 重试 / 脚本列表出口
- 主动检查由本地 pending 立刻接管忙态,并把服务端的「正忙」「结果够新已跳过」
  「通道异常」三条回执分别说出来;跳过时就地清掉待反馈标记,
  否则会在下一次后台检查完成时冒出一条用户没点过的 toast
- 忽略复用与更新相同的行级阶段(working → success → 退场),不再 fire-and-forget
- 批量进行中互斥(行内勾选、两个批量按钮、全部恢复),避免两条进度互相覆盖;
  被「结果失效」中断时保留已完成条数,不把汇总抹掉
- 骨架补齐工具条(桌面)与顶部选择栏/底部操作栏(移动)占位,消除数据到达时的
  布局跳动,并加 role="status" / aria-busy;空态下重新检查不再整页闪回骨架
- 脚本名改用 aria-disabled + onClick 早退:disabled 会让浏览器不派发指针事件,
  正好在名字被截断、最需要看全名时把 tooltip 一起关掉,键盘触发后焦点还会掉到 body
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant