尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
DeepChat Chat Scroll Ownership 重构实战:从 3000 行 ChatPage 到单滚动所有者状态机
DeepChat Chat Scroll Ownership 重构实战从 3000 行 ChatPage 到单滚动所有者状态机【免费下载链接】deepchatDeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat本篇技术指南以 DeepChat 仓库中 chat-scroll-ownership/tasks.md 的任务清单为主轴完整梳理「Chat 滚动所有权」重构的八个阶段从基线取证、纯状态机、会话预加载到控制器接入、隔离页面几何、特性兼容与性能清理。读完本文你将掌握如何为一个超大型 Vue 渲染组件建立单一滚动所有者架构用 typed request、session epoch 与独占仲裁器根治「闪跳、回滚、滚动条被抢」等浏览器布局难题并了解仓库中对应的源码实现与验收标准。背景为什么滚动需要「所有权」而不是「时间窗口」重构的起点是 spec.md 中描述的核心问题ChatPage.vue在超过 3,000 行的单组件里同时承担页面编排、消息转换、会话恢复、历史分页、有界渲染、行测量、自动跟随、搜索、Spotlight 导航、计划布局、编辑器行为与直接 DOM 滚动。更致命的是同一个外层元素既是页面外壳又是消息滚动容器。其滚动几何因此包含了顶栏、搜索框、消息、计划内边距、pending 车道、状态内容和吸底编辑器。这些区域的高度变化与消息无关却会让 Chromium 在 JavaScript 没有显式请求滚动时自行钳制scrollTop。同时多个代码路径各自通过scrollTop或scrollIntoView写滚动位置所有权只能靠重叠的毫秒时间窗口推断jsdom 单测无法复现真实 Chromium 的布局、sticky 定位、原生滚动锚定、合成器行为与滚动钳制。因此本项目在 plan.md 中确立了九大目标核心三条为让消息视口成为唯一拥有聊天滚动的元素所有程序化滚动经由一个带显式 reason 与 priority 的 typed controller 路由活跃用户手势获得持久所有权直到用户显式返回底部或发起导航。阶段总览八阶段渐进迁移而非一次性重写tasks.md 将整个迁移切分为八个阶段每个阶段保持仓库可构建、既有测试保持绿色避免长时间并存两套生产滚动实现阶段主题状态截至 2026-07-15 左右Phase 0基线与浏览器取证进行中Phase 1纯所有权模型已完成Phase 2会话准备与性能基线已完成Phase 3当前 DOM 上的控制器集成已完成Phase 4隔离页面几何进行中Phase 5特性兼容待办Phase 6性能与清理进行中Phase 7验证与交接进行中Phase 8评审加固已完成Phase 0先取证再动手改架构Phase 0 强调「先复现、再重构」添加仅开发环境生效的滚动插桩记录 reason、request ID、session epoch、期望 top、实际 top、scroll height、client height 与 anchor ID添加可实际驱动 ChatPage 布局与原生滚动的真实 Chromium 测试 harness在改动架构前先复现并记录「四条消息回滚」问题为恢复、流式、编辑器 resize、计划、搜索、Spotlight、历史、编辑器聚焦与长窗口测量建立基线场景。这一步对应 plan.md 中的迁移顺序第一步先建立浏览器回归 harness 与滚动写插桩度量当前行为后续每步迁移都以此对照。仓库中对应的 opt-in Electron 场景位于test/e2e/specs/31-chat-scroll-ownership.smoke.spec.tsPlaywright 已能成功发现它但真实 Electron/Provider 场景与 macOS 触控板手动矩阵仍待完成。Phase 1纯所有权模型 —— 状态机、优先级与独占仲裁Phase 1 是全部工作的理论地基全部标为完成。它交付了四样东西typed 滚动原因/目标/请求/状态、纯滚动状态机、一帧一写的请求队列、跨帧只有一个活跃所有者的独占操作仲裁器。Typed 原因与请求在源码 chatScrollState.ts 中滚动原因被限定为以下联合类型export type ChatScrollReason | session-restore | auto-follow | submit | history-prepend | measurement-anchor | history-navigation | search-navigation | spotlight-navigation | user-return-to-bottom比 spec 中的早期清单多了history-navigation一项可见评审阶段对历史导航语义的收敛。每个请求携带id与sessionEpoch目标分为四类export type ChatScrollTarget | { kind: bottom } | { kind: absolute; top: number } | { kind: message; messageId: string; align: start | center | one-third }显式且稳定的优先级chatScrollState.ts 用getChatScrollRequestPriority把原因映射为数值与 spec 的优先级列表一一对应优先级原因说明100user-return-to-bottom / history-navigation / search-navigation / spotlight-navigation显式用户导航90submit提交新消息80history-prepend历史前插锚点保持70session-restore会话恢复被用户打断前60auto-follow生成自动跟随50measurement-anchor被动测量精化纯状态机与可接受性状态机输入是ChatScrollEventbegin-session、user-gesture-start/end、bottom-proximity-changed、return-to-bottom、submit-started、history-navigation-start、explicit-navigation-start、restore-requested/complete、stream-updated、viewport-resized、measurements-committed、request-committed 等输出是新的ChatScrollStatemode、userOwned、nearBottom、activeGesture、lastCommittedRequestId 等完全无 DOM 依赖可用 jsdom 之外的最小环境逐事件单测。关键规则体现在canAcceptChatScrollRequestrestore 只有在未被用户拥有、且无显式导航hasExplicitNavigation为假时才可接受防止「用户在搜索/Spotlight 跳转后 restore 又把视口拽回底部」history-prepend 与 measurement-anchor 都要求userOwned且mode reading即被动操作永远不会抢占用户。独占操作仲裁器与单槽请求队列chatScrollOperationArbiter.ts 实现「任意时刻最多一个活跃操作」同原因请求合并replace、更高优先级原子替换返回被替换的 requestId、更低优先级直接丢弃而非延迟重放。会话 epoch 不匹配的请求一律拒绝。chatScrollRequestQueue.ts 则是一个单槽 pending 队列take(sessionEpoch)只在 epoch 匹配时弹出评审加固后Phase 8队列采用单调递增的 epoch 排序stale take 时会保留更新的 pending 工作。Phase 2会话准备与性能基线Phase 2 聚焦「首帧体验」与「会话切换体验」全部完成为 selection、preparation、commit、first message paint、input readiness、secondary state completion 添加 performance marks —— 源码见 chatSessionPerformance.ts标记格式为deepchat:chat-session:${phase}并携带sessionId与sessionEpoch且明确「插桩绝不影响聊天就绪」引入不可变PreparedChatSessionViewsessionId、sessionEpoch、messageRevision、messageIds、messageCache、cursor、hasMoreHistory、可选 layoutSnapshot 与 viewportAnchor带 stale-epoch 拒绝把「先清空再加载」替换为一次原子目标会话提交杜绝「新会话头 旧会话消息」的混合帧允许消息就绪先于 pending inputs、plans 与 metadata 解除绘制阻塞添加渲染端有界 LRU最多五个会话视图 内存/数量预算含 cache revision 失效与驱逐测试添加冷启动、缓存切换、未缓存切换、快速 A/B/C 竞态测试。spec 中给出了清晰的 critical / secondary 路径划分critical path session selection - prepare latest message window - atomically commit session view - render latest bounded rows - position once - input and message viewport interactive secondary path pending inputs plans metadata adjacent pre-measurement optional pre-hydration性能验收标准spec 第 13 条要求参考机器上热缓存会话切换首次有效消息绘制 p95 ≤ 100ms未缓存本地切换 ≤ 250ms初始聊天外壳可交互 ≤ 150msCI 记录这些指标并对绝对预算放宽以容忍机器方差。Phase 3控制器接入当前 DOMPhase 3 在不动 DOM 结构的前提下把滚动权逐步收归useChatScrollController全部完成围绕当前视口接入 controller会话恢复、提交与流式自动跟随、历史补偿与测量锚定、搜索与 Spotlight 导航全部改走 controller程序化滚动事件改用request ID / 期望目标匹配而非时间窗口新增聚焦所有权测试禁止在 controller 之外对消息视口产生新的直接写。源码 useChatScrollController.ts 是这套逻辑的落点也是唯一允许给视口赋scrollTop的低层模块。其公开 API 包括export function useChatScrollController(options: { viewport: ReadonlyShallowRefHTMLElement | null resolveMessageTop: (messageId: string) number | null bottomThreshold?: number // 默认 80px canAutoFollow?: () boolean onCommitted?: (top: number, request: ChatScrollRequest) void })返回的 controller 暴露state、activeOperation、beginSession、request、requestImmediate、notifyUserGestureStart/End、notifyViewportScroll/Resize、dispose。在 ChatPage.vue 中可以看到实际接入导入useChatScrollController会话切换时调用chatScrollController.beginSession(id)取得新 epoch滚轮/触摸等手势经onGestureStart回调转发为notifyUserGestureStart(kind)。一帧一写与验证帧controller 内部的关键机制scheduleCommit通过requestAnimationFrame排入提交帧writeCommittedThisFrame保证一帧至多一次物理写即使没有第二个请求等待也在下一帧边界过期立即写保护提交时resolveTargetTop计算目标 topbottom 取scrollHeight - clientHeightabsolute 做 0..maxTop 钳制message 目标按 align 计算 center/one-third 偏移若与当前scrollTop差 1px 则跳过物理写提交后记录{ request, expectedTop }下一次notifyViewportScroll用1px 容差比对匹配则视为程序化回执并完成操作不匹配则归类为用户/原生几何事件——彻底替代旧的programmaticScrollUntil/userScrollInputUntil毫秒窗口判断。手势的原子取消notifyUserGestureStart一次性完成取消已排帧、清空队列、arbiter.cancelAll()、清空 committedScroll、触发user-gesture-start状态迁移mode → reading、userOwned → true。用户手势对活跃操作与所有未提交工作是「一步取消」而不是依赖空闲定时器——spec 明确「空闲定时器只能降低测量/渲染工作量绝不能决定滚动条归谁所有」。Phase 4隔离页面几何 —— 三行网格外壳Phase 4 的核心是把「外壳与滚动容器」拆开。已完成部分包括搜索与历史状态移到消息流之外的 overlay把顶栏、计划内边距、pending 车道、状态、编辑器从消息滚动范围中移除按 controller 模式在 header/composer 的 ResizeObserver 变化中保持视口锚点。进行中的部分则是引入三个新组件ChatPageShell.vueheader、viewport、composer 三区域ChatMessageViewport.vue唯一拥有聊天 overflow 的元素ChatComposerRegion.vue不改子组件挂载生命周期与公开行为的前提下搬移 composer。spec 规定的目标布局如下ChatPageShell (overflow: hidden) ├── ChatHeaderRegion fixed layout row; never scrolls with messages ├── ChatMessageViewport minmax(0, 1fr); the only overflow-y:auto owner │ ├── history status overlay out of normal message flow │ ├── search overlay out of normal message flow │ └── MessageList messages and virtual spacers only └── ChatComposerRegion fixed layout row; outside message scroll geometry ├── pending input lane ├── plan / interaction layer ├── ChatInputBox └── ChatStatusBar外壳使用display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden只有ChatMessageViewport可以overflow-y: auto。头部、composer、计划、状态、pending-input、搜索的高度变化不再改变消息滚动范围内的内容视口尺寸变化以几何事件上报 controllernotifyViewportResize而不是被误判为用户滚动。notifyViewportResize内部按 mode 分派restoring 发 session-restore、following 发 auto-follow且都遵守canAutoFollow用户拥有时直接返回 null 不写。Phase 5特性兼容矩阵 —— 全量行为保持不变Phase 5 全部为待办但它定义了重构的「不破坏清单」也是验收的对照表。spec 的兼容矩阵要求以下能力逐一保持会话打开/切换最新消息出现在底部除非用户打断恢复首次应用聊天加载外壳与输入先于次要会话状态可交互缓存会话切换近期会话视图与锚点即时恢复、无空态闪烁未缓存会话切换固定视口加载态随后一次原子目标会话提交自动滚动开启/关闭开启时跟随至用户离开底部关闭时永不抢视口新消息提交新回合一次性移动到底部之后尊重用户所有权短会话无分页、无测量修正、无闪烁回滚长会话有界挂载行、稳定 spacer、无空白缝隙旧历史加载同一条消息与消息内偏移保持可见Cmd/CtrlF完整已加载消息计数next/prev 跨虚拟窗口可用Spotlight/trace 跳转目标挂载、居中一次、高亮、清除 pending 导航流式 Markdown阅读模式下不重建列表、不移动视口图片/artifact/工具延迟尺寸变化保持长列表锚点短列表零写计划/交互/pending 车道视觉行为不变不参与消息滚动范围composer resize/focus/IME草稿与焦点保持消息视口不被滚动只读/subagent 会话、捕获/导出显示与导航行为、完整已加载消息捕获能力保持不变。Tasks 中进一步细化为十余条勾选项特别强调「TipTap 滚动被限制在编辑器内部」「在渐进挂载有界重型行的同时保持已加载历史/搜索计数」「会话切换全程保持外壳、composer、TipTap 与输入就绪」。Phase 6性能与清理 —— 短列表零写长列表有界Phase 6 已完成的部分体现了性能要点短列表1–20 条消息完全渲染且零测量滚动写长列表挂载行保持有界并优化消息条目查找避免热路径上反复全数组搜索对应 plan 中的 message ID → layout entry 缓存布局读取、测量与每帧一次视口写批量处理删除遗留滚动定时器、重复布尔量、直接写 helper 与过时注释。待办项包括把 ChatPage 收敛为编排壳并抽取所有权清晰的特性 composables验证无滚动状态 CSS 变更引发大 repaint 或合成器抖动在参考机器上达标初始外壳、缓存切换、未缓存切换延迟预算验证近期会话缓存内存保持在配置预算内。性能验收标准的其余条目同样值得引用spec 第 1–12 条活跃用户手势期间零未授权写阅读模式下逻辑锚点漂移 ≤ 1 CSS 像素一次用户导航至多一次视口写 一次纯高亮 DOM 遍历每帧至多一次提交写跨帧至多一个活跃事务且无第二所有者交替写连续滚轮/触控板滚动无 50ms 以上应用长任务、无逐帧全量列表转换流式滚动工作按帧合并token 频率 ≠ 写频率搜索/历史/Spotlight 导航无中间窗口交换。Phase 7验证与交接Phase 7 的已完成项包括全部纯状态/controller 测试通过、ChatPage 与消息窗口组件测试通过、全量渲染套件通过spec 记录168 个文件、1267 个测试4 worker、单测 30 秒预算、format/i18n/lint/Web typecheck/build 全绿。待办项提示了「真实浏览器」与「真实设备」的边界真实 Chromium 滚动矩阵锚点漂移 ≤ 1px、macOS 短/长对话触控板与滚轮手动验证、前后性能与滚动写指标记录、冷首载/热缓存/未缓存/快速切换竞态指标记录以及把最终实现结果回写本规范与 retained scroll-coordinate issuedocs/issues/chat-history-search-scroll-coordinates/spec.md。spec 明确目前尚未对最终物理设备验证作出任何声明——这是判断本项目进度的准确依据。Phase 8评审加固 —— 竞态与所有权的不变量Phase 8 全部完成是代码质量最密集的一轮tasks 列出的关键项与源码一一对应请求队列 epoch 排序单调stale take 保留更新的 pending 工作见 chatScrollRequestQueue.ts立即写帧保护在下一帧边界过期见 controller 中scheduleCommit后的注释用户所有权跨原生布局滚动与合并显式导航保持resize 驱动的自动跟随受autoScrollEnabled门控但不阻塞初始恢复失败或已被取代的会话准备停留在安全的 committed-view 边界之后绝不暴露目标视图或启用其 composer触摸所有权跨原生惯性滚动保持活跃并在不依赖后续scroll事件的情况下武装顶部翻页对应 plan 中「touch ownership 持续到scrollend idle 兜底」近期测量快照跨 keyedChatPageremount 生命周期持久Electron 搜索断言从纯时间窗改为可观察完成态每个评审加固不变量都补充回归测试并重跑全部质量门禁committed message readiness 改为 store-owned并以 live mutation revision 围栏同会话刷新对流式终止身份、pending-input 写入、历史重叠、提交延续与 A-B-A 会话水合全部加 fence。spec 总结的评审加固成果还包括review-hardening 的 scroll/page/keyed-parent/cache/architecture 套件120/120 通过useChatScrollController成为唯一低层消息视口写入者被动所有者按模式门控restore/follow 不能在 reading 模式运行measurement anchor 不能在 following 模式运行。数据与兼容性保证无迁移的源码级回滚重构刻意把改动圈定在渲染端布局与瞬时状态内。spec 的数据保证明确不触碰src/main、src/preload、数据库 schema、加密、导入导出与持久化配置不改 message ID、renderKey、排序、分页 cursor、会话身份与消息缓存语义autoScrollEnabled保留既有持久化 key 与含义controller 状态仅存在于渲染端、随会话 epoch 重置、永不持久化。因此回滚是纯源码回滚不需要数据回滚——这也是 Phase 6/7 能放心逐阶段推进的前提。质量门禁与测试策略速查无论推进到哪个阶段仓库的 plan.md 都要求以下门禁全绿聚焦纯/controller 测试、既有 ChatPage 与消息窗口套件、真实 Chromium 滚动套件、全量渲染套件以及pnpm run format、pnpm run i18n、pnpm run lint、pnpm run typecheck:web、pnpm run build。相关测试可在 test/renderer/composables/chat/chatScrollArchitecture.test.ts、useChatScrollController.test.ts与 ChatPage.test.ts 中找到。关键的风险对冲同样值得记取composer 抽取破坏 TipTap/IME → 只搬 DOM 不改挂载生命周期并补焦点/草稿测试搜索/Spotlight 丢失窗口外目标 → 保留逻辑消息寻址与 mount-before-highlight 流程迁移期出现两个冲突所有者 → 聚焦所有权测试 分阶段删除直接写近期会话缓存内存膨胀 → 小 LRU 内存预算 可观测驱逐。结语一份可复用的滚动所有权重构清单从 tasks.md 的 8 个阶段可以提炼出一条通用方法论先取证Phase 0、再建纯模型Phase 1、后铺性能地基Phase 2、逐路径收权Phase 3、最后动几何Phase 4并以兼容矩阵Phase 5、性能清理Phase 6、真实浏览器验证Phase 7与竞态加固Phase 8收尾。核心纪律只有三条typed request session epoch 替代时间窗口、独占仲裁器 一帧一写替代多路径直接写、用户手势原子取消替代空闲定时器。这套模式对任何「单页壳 长列表 流式内容」的应用都有直接的借鉴价值而 DeepChat 的这份文档与源码就是最完整的落地样本。【免费下载链接】deepchatDeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

Rerun Position2D 组件深度解析:2D 空间坐标的编码、序列化与多语言使用

Rerun Position2D 组件深度解析:2D 空间坐标的编码、序列化与多语言使用

Rerun Position2D 组件深度解析:2D 空间坐标的编码、序列化与多语言使用 【免费下载链接】rerun Visualize, query, and stream to train on multimodal robotics data. 项目地址: https://gitcode.com/GitHub_Trending/re/rerun 导读 Position2D 是 Rerun …

📅 2026/9/17 22:38:57
fluent-bit 内置 nghttp2 的 HPACK 头字段压缩:nghttp2_hd_deflate_hd 函数深入解析

fluent-bit 内置 nghttp2 的 HPACK 头字段压缩:nghttp2_hd_deflate_hd 函数深入解析

fluent-bit 内置 nghttp2 的 HPACK 头字段压缩:nghttp2_hd_deflate_hd 函数深入解析 【免费下载链接】fluent-bit Fast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows 项目地址: https://gitcode.com/GitHub_Trending/fl/…

📅 2026/9/17 22:38:57
Kubernetes Goat 私有容器镜像仓库攻击实战:利用 Registry HTTP API v2 拉取镜像元数据与敏感凭证

Kubernetes Goat 私有容器镜像仓库攻击实战:利用 Registry HTTP API v2 拉取镜像元数据与敏感凭证

Kubernetes Goat 私有容器镜像仓库攻击实战:利用 Registry HTTP API v2 拉取镜像元数据与敏感凭证 【免费下载链接】kubernetes-goat Kubernetes Goat is a "Vulnerable by Design" cluster environment to learn and practice Kubernetes security using…

📅 2026/9/17 22:38:57
MORE NEWS

更多资讯

📰

隔壁开了同品类怎么办?小吃店竞争应对的四个动作

【本篇要点】 先别降价:价格战直接吃利润,而且容易陷入互相压价的死循环。 在顾客能感知的地方拉开差距,比在价格上纠缠更有效。 竞争对手是免费的调研样本,他验证过的做法可以直接借鉴。开小吃店很现实的一件事:你生意…

📰

Wand-Enhancer 本地增强完整指南:一键解锁 Pro 订阅,手机远程面板开箱即用

Wand-Enhancer 本地增强完整指南:一键解锁 Pro 订阅,手机远程面板开箱即用 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer …

📰

长沙烧烤培训:素菜与特色串怎么把菜单做厚

【本篇要点】 素菜成本低、出餐快、承担解腻角色,是完全的增量收入。 特色串是摊位招牌,难度更高但一旦做好就是溢价来源。 菜单分三层:基本盘、走量素菜、特色招牌,扩张节奏看经营数据。烧烤的腌料与火候是基本功,这一…

📰

Gutenberg Interactivity API 客户端导航实战:interactivity-router 的区域路由、预取与源码级实现解析

Gutenberg Interactivity API 客户端导航实战:interactivity-router 的区域路由、预取与源码级实现解析 【免费下载链接】gutenberg The Block Editor project for WordPress and beyond. Plugin is available from the official repository. 项目地址: https://g…

📰

2026主流企业邮箱单用户年费收费明细

中小微企业选购企业邮箱,核心参考指标集中在单用户年费、功能权限、存储容量、域名适配四大维度。市面上多数品牌采用打包售卖模式,整体报价容易掩盖单用户真实成本。很多企业采购时,容易出现预算超支、功能冗余或核心功能缺失的问题。本文结…

📰

提示词攻击生成器把 Anthropic 地址改到 TaoToken 后批量生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬