Skip to content

fix(frontend): scroll-view 卸载后仍写 scrollTop——给 uni-h5 补空判(dev-board#349) - #690

Merged
zeweihan merged 1 commit into
masterfrom
fix/uni-h5-scrollview-null-guard-349
Sep 1, 2026
Merged

fix(frontend): scroll-view 卸载后仍写 scrollTop——给 uni-h5 补空判(dev-board#349)#690
zeweihan merged 1 commit into
masterfrom
fix/uni-h5-scrollview-null-guard-349

Conversation

@zeweihan

@zeweihan zeweihan commented Sep 1, 2026

Copy link
Copy Markdown
Owner

修 dev-board#349。

根因与卡上的猜测不一样

卡上猜触发点是 ChatInterface.vuescrollTop.value += 10000(约 1214 行)与 FileStagingArea.vue。读了框架源码并核对堆栈行号后,两个都不是

  • 崩的那行是 uni-h5.es.js:14370main.value.scrollTop = val,位于 _scrollTopChanged不带动画的那条分支。ChatInterface 的 scroll-view 显式设了 :scroll-with-animation="true"ChatInterface.vue:248),走的是另一条 scrollTo 分支,压根到不了这一行
  • FileStagingArea.vuescrollTopdata() 初始化成 0 之后,全文件再没有任何一处写它。
  • 调用方是堆栈里的第二帧 14483
    onMounted(() => {
      nextTick(() => {
        _scrollTopChanged(scrollTopNumber.value);   // <- 14483
        _scrollLeftChanged(scrollLeftNumber.value);
      });
    这是挂载时框架自己排的那一次补写,与我们代码里写不写 scrollTop 完全无关。

也就是说:任何 <scroll-view scroll-y> 只要在同一批 flush 里挂载又卸载(v-if 分支翻转、路由离开),这个 nextTick 回调跑到时 main.value 已经是 null,就崩。复现路径上的那个是统一设置页 AdminPane.vue:1124 渲染的 PersonalWorkLogPanel.vue:36 —— <scroll-view v-else scroll-y>,挂在 loading / 空 / 有数据三分支上。

两条错误是同一个根因:Vue 打出 TypeError,nextTick 的 promise 链再把它变成一条未处理 rejection,被 main.js:31 的全局兜底打成第二条。

修法

框架层没有空判,而我们无法取消它已经排好的 nextTick,只能补这道守卫;语义上也正是对的——元素都没了,本来就没有什么可滚的。照 desktop/scripts/patch-dmg-builder.js 的既有套路加一个 postinstall 补丁(frontend/scripts/patch-uni-h5-scrollview.mjs),对 uni-h5.es.js / uni-h5.cjs.js 各加三处真值判断:

  1. _scrollTopChanged 的直写;
  2. _scrollLeftChanged 的直写;
  3. 动画分支 scrollTo 的入口 —— 聊天区自动滚动就在这条路上,同一个卸载竞态会崩在 container.scrollWidth,一并守住。

锚点在目标文件里必须唯一,对不上就 exit 1 让安装失败(升级 uni-h5 后报「结构已变」时先看新版是否已自带空判,是就删掉补丁与钩子)。产物侧靠 npm ci 触发 postinstall —— ci.yml 的 frontend job 与 desktop-build.yml 的 Build frontend 步骤都会跑到。

没有动 ChatInterface.vue 一个字:卡上担心的「改坏聊天区自动滚动这条高频 UI 路径」这条风险不存在。

回归护栏

加在 tests/app-e2e/run.mjs 结尾。控制台异常信号整体不判死(历史噪音多,一刀切会天天红),但 setting 'scrollTop' 这一条单拎出来判死 —— 不然补丁哪天掉了也只是报告里多两行没人看。

验证(冷启动隔离环境:后端 9797 + dev server 5175,配方见 eng-infra)

定点探针,真实鼠标点击走「项目列表页 → 个人中心」:

加载项目列表页 跳转到设置页后
改前 0 条信号 2 条未处理的 Promise rejection + TypeError: Cannot set properties of null (setting 'scrollTop')
改后 0 条信号 0 条

全量 npm run test:app-e2e,两轮对照

步骤 异常信号 退出码
打了补丁 131 通过 / 0 失败 4 条(1 info + 2 环境 skip + 1 条 J1 上的既有 console 错误) 0
还原病灶(把补丁从 node_modules 摘掉) 131 通过 / 0 失败 5 条,多出的正是那两条,出现在 J2 项目列表页 + 个人设置 1,并打出新护栏的提示

摘补丁时校验过三个锚点各自唯一、改后形态存在。两轮的 131/0 一致,说明滚动行为没坏——差异只在这两条信号上。

(打补丁那轮多出的 [console] JSHandle@error 出现在 J1 首启解锁门,在跳设置页之前;还原病灶那轮它没出现,是条间歇性的既有信号,与本改动无关。)

复测提示

打开开发者工具跳一趟「项目列表页 → 设置」,控制台不应再出现 Cannot set properties of null (setting 'scrollTop') 与随之而来的未处理 rejection;聊天区发消息后仍应平滑滚到底。

🤖 Generated with Claude Code

卡上猜是 ChatInterface.vue 的 `scrollTop.value += 10000` 与 FileStagingArea.vue。
读了框架源码 + 抓了真实堆栈,两个都不是:

- 崩的那行是 `uni-h5.es.js:14370` 的 `main.value.scrollTop = val`,属于
  `_scrollTopChanged` 里**不带动画**的那条分支。ChatInterface 的 scroll-view 显式设了
  `:scroll-with-animation="true"`,走的是另一条 `scrollTo` 分支,压根到不了这一行。
- FileStagingArea 的 scrollTop 从初始化后再没被写过。
- 调用方是 `14483`:`onMounted(() => nextTick(() => _scrollTopChanged(scrollTopNumber.value)))`
  ——**挂载时**框架自己排的那一次补写,与我们代码里写不写 scrollTop 无关。

也就是说:任何 `<scroll-view scroll-y>` 只要在同一批 flush 里挂载又卸载(v-if 分支翻转、
路由离开),这个 nextTick 回调跑到时 `main.value` 已经是 null,就崩。复现路径上是
统一设置页 AdminPane 里 PersonalWorkLogPanel 的
`<scroll-view v-else scroll-y>`(loading / 空 / 有数据三分支)。两条错误同一个根因:
Vue 打出 TypeError,nextTick 的 promise 链再把它变成一条未处理 rejection,
被 main.js 的全局兜底打成第二条。

框架层没有空判,而我们**无法取消**它已经排好的 nextTick,只能补这道守卫。语义上也正是
对的:元素都没了,本来就没有什么可滚的。照 desktop/scripts/patch-dmg-builder.js 的既有
套路,加 frontend/scripts/patch-uni-h5-scrollview.mjs + frontend package.json 的
postinstall,对 uni-h5.es.js / uni-h5.cjs.js 各加三处真值判断:两条 `_scroll*Changed`
的直写,外加动画分支 `scrollTo` 的入口(聊天区自动滚动就在那条路上,同一个卸载竞态会崩在
container.scrollWidth,一并守住)。锚点在文件内必须唯一,对不上就 exit 1 让安装失败——
升级 uni-h5 后报「结构已变」时先看新版是否已自带空判。

**没有动 ChatInterface.vue 一个字**:卡上担心的「改坏聊天区自动滚动」这条风险不存在。

回归护栏加在 tests/app-e2e/run.mjs 结尾:控制台异常信号整体不判死(历史噪音多,一刀切
会天天红),但 `setting 'scrollTop'` 这一条单拎出来判死——不然补丁掉了也只是报告里多两行
没人看。

冷启动隔离环境(后端 9797 + dev server 5175,配方见 eng-infra):
- 定点探针走「项目列表页 →(真实鼠标点击)个人中心」:**改前 2 条信号,改后 0 条**,
  加载项目列表页本身 0 条(信号确实来自这次跳转)。
- 打补丁后全量 `npm run test:app-e2e`:**131 步通过 / 0 失败**,scrollTop 信号 0 条。
  剩下 4 条异常信号是 1 条 info + 2 条环境 skip + 1 条 J1 解锁门上的既有 console 错误
  (出现在跳设置页之前,与本改动无关)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zeweihan
zeweihan force-pushed the fix/uni-h5-scrollview-null-guard-349 branch from b35f1b5 to 5a93f0a Compare September 1, 2026 04:14
@zeweihan
zeweihan merged commit 1f7ffa2 into master Sep 1, 2026
8 checks passed
@zeweihan
zeweihan deleted the fix/uni-h5-scrollview-null-guard-349 branch September 1, 2026 05:00
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant