Plate Slate v2 实战:异步 decorate 导致光标漂移的根因定位与浏览器级 Proof 设计

发布时间:2026/9/17 11:32:43
Plate Slate v2 实战:异步 decorate 导致光标漂移的根因定位与浏览器级 Proof 设计 Plate Slate v2 实战异步 decorate 导致光标漂移的根因定位与浏览器级 Proof 设计【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本篇以 Plate 仓库中的 异步 decorate 光标簇验证计划 为主体完整还原 Slate 上游问题 #5987decorate因异步状态更新而改变时光标会跳回在 Slate v2 fork 中的复现路径、修复方案与浏览器集成证明browser proof设计并延伸到仓库中的 架构跟进评审 与 解法沉淀文档。读完后你能掌握如何为仅重排 DOM 结构、不产生编辑器提交的渲染变更设计选择权修复selection repair机制以及如何用 Playwright 同时对 Slate 模型选择和浏览器 DOM 光标做双轨断言。问题背景decorate的异步更新与光标跳变该计划文档处理的是 Slate issue ledger 中的 Cluster 10对应上游 issue#5987当decorate从一个异步状态更新中改变时光标会跳到错误位置。上游 PR#6033通过让装饰decoration重排与Editable的选择权selection恢复保持同步来修复了同一失败模式。计划文档中记录的复现场景非常具体编辑器内容为This is some text here about. there光标位于行尾用户继续键入there一个延迟出现的高亮decoration随后应用到文本上此时光标在about.与there之间向后跳回。文档同时给出了两条重要的既有状态证据修复前 ledger 只能声明Improves #5987因为当时的证明只覆盖了投影来源projection source稳定性并没有针对精确的异步Editable.decorate复现路径相关 Slate v2 表面仍然保留Editable.decorate这一兼容 prop因此精确证明必须针对这个适配层adapter本身而不只是 v2 一等公民的decorationSources路径。这正是装饰类 bug 的典型困难装饰应用会改变 DOM 结构或叶子leaf边界而选择权映射与输入法IME合成时序对这种变化极其敏感外部时序驱动的装饰更新会进一步放大问题——仓库的装饰/注解簇文档 decorations-annotations-cluster 把这一族问题#3309、#3162、#4712、#5987、#4581 等归纳为装饰后的 DOM 与编辑器状态漂移与本计划同属一个根因家族。Claim Bar把Improves升级为Fixes的四条硬标准计划文档定义了一个可审计的声明门槛Claim Bar只有当浏览器集成测试同时证明以下四点才允许把 issue 声明从Improves #5987升级为Fixes #5987Editable接收到的decorateprop 是一个函数身份function identity在异步状态更新之后才改变的函数用户在延迟装饰应用之前已经在编辑器末尾键入了匹配文本延迟装饰可见地重排了渲染后的文本DOM 结构确实变化了延迟装饰应用之后浏览器选择权与 Slate 选择权都保持在键入处的文档末尾。任何一条不满足issue 只能停留在Improves #5987。这个门槛的设计价值在于它拒绝了假绿fake green——后文的解法文档明确指出只检查 Slate 模型选择权会得到假绿模型选择权本身是正确的但真实 DOM 光标是错的。执行计划与结果计划文档中的五步执行清单全部完成新增一个镜像上游 issue 路径的decorations-async示例新增一条 Playwright 回归在末尾键入 → 等待延迟高亮出现 → 断言光标稳定仅当新测试行失败时才修改slate-react本次实际修改了对新测试行执行聚焦的浏览器验证用精确证明与最终声明更新 Cluster 10 的 ledger。修复前的浏览器测试行复现了精确的失败两个关键数字Slate 模型选择权停留在偏移量41键入后的文档末尾正确浏览器 DOM 光标停留在偏移量35旧装饰文本的结尾处错误。这两个数字的落差就是根因的精确刻画文档模型没变没有编辑器提交但装饰刷新把文本 DOM 切开并包上了高亮节点DOM 字符偏移映射随之改变而 DOM 光标仍钉在旧的装饰边界上。Changeset 判定计划文档还给出了 changeset 决策规则若本次只新增站点示例、测试与 ledger 更新则不需要 changeset但如果.tmp/slate-v2/packages下的包运行时代码发生变化则必须在收尾前判定 changeset 必要性。本次运行时代码确实变更于slate-react因此 Slate v2 checkout 中包含.changeset/async-decorate-caret.md而 Plate 侧不需要 changeset。修复方案投影刷新后强制一次 repair render仓库中对应的解法沉淀文档 2026-05-23-async-decorate-refresh-must-export-dom-selection.md 把机制讲透了根因Editable.decorate可以在用户键入之后由一次异步 React 状态更新而改变。装饰刷新会切开并包裹文本 DOM但不会创建 Slate 编辑器提交document 本身没变因此由提交驱动的 DOM 选择权导出这条正常路径根本不会执行。为什么常规思路不成立该文档的 What Didnt Work 部分仅保证 projection-source 稳定性不够文本渲染是正确的但浏览器光标仍跟随旧 DOM 边界只断言 Slate 模型选择权是假绿模型选择权已经正确真实光标却是错的。修复让投影刷新报告渲染后的投影桶是否发生了变化editable 运行时只订阅一次这些刷新结果并在非编辑器来源的投影刷新改变了渲染文本时请求一次渲染修复repair renderreturn projectionStore.subscribeProjectionRefresh((result) { if (!result.requiresDOMSelectionExport) return requestEditableRepair({ forceRender: true, kind: force-render, selectionSourceTransition: { preferModelSelection: true, reason: projection-refresh, selectionSource: model-owned, }, }) })关键在于preferModelSelection: true修复渲染完成后选择权导出以模型选择权偏移量 41为准重新落到 DOM 上。由于纯装饰性质的 DOM 变更不产生编辑器提交这一步为选择权导出提供了在投影文本 DOM 已重排之后的一个渲染通道使装饰 DOM 重排与选择权恢复落在同一个 repair 窗口内——这与上游 #6033 的根因结论一致。浏览器级回归证明双轨断言写法回归测试放在浏览器套件而不是模型级单元测试中解法文档原文回归住在浏览器套件不是 model-only unit test。测试核心是模型选择权 DOM 光标双轨断言await page.keyboard.type( there) await expect(page.locator([data-cyasync-decoration-highlight])).toHaveCount(3) await editor.assert.selection({ anchor: { path: [0, 0], offset: 41 }, focus: { path: [0, 0], offset: 41 }, }) await expect .poll(() getDOMCaretOffsetInFirstText(editor.root)) .toEqual({ offset: 41, text: This is some text here about. there there, })逐行看这段证明在验证 Claim Bar 的哪一条page.keyboard.type( there)对应标准第 2 条在异步装饰应用前于编辑器末尾键入等待async-decoration-highlight出现 3 个对应标准第 3 条延迟装饰可见地重排了渲染文本editor.assert.selection断言 Slate 模型选择权锚点与焦点都落在[0, 0]路径偏移41poll(getDOMCaretOffsetInFirstText)轮询真实 DOM 光标偏移与所在文本断言其同样到达41且文本为键入后的完整字符串。只有两个断言同时通过才说明模型选择权与 DOM 光标在延迟装饰重排后仍保持在键入处的文档末尾标准第 4 条。架构跟进从adapter 直接 force render到投影刷新契约该仓库没有止步于一个能跑通的补丁。跟进评审文档 2026-05-23-slate-v2-projection-refresh-selection-repair-ralplan.md 对第一版修复给出了明确判定当前修复是好的回归修复但不是最终最优架构当前评分 0.82目标架构 0.94。问题在于第一版让 legacy 适配层直接调用EDITOR_TO_FORCE_RENDER.get(editor)?.()这证明根因但把Editable.decorate适配层耦合到了 editable repair 渲染器上。评审用源码行级证据定位了耦合点ledger 记录于.tmp/slate-v2checkoutpackages/slate-react/src/components/editable-text-blocks.tsx中创建 legacy 装饰来源、在decorate身份变化时刷新来源并直接触发 force renderprojection-store.ts已具备正确的概念性所有者projectionStore.refresh(...)而use-slate-decoration-source.ts的一等装饰来源存在同样的外部刷新形态——所以架构问题不能只在Editable.decorate一处解决。目标架构的核心是一个内部契约类型type SlateProjectionRefreshResult { changedRuntimeIds: readonly RuntimeId[] changedSourceId?: string didChange: boolean reason: SlateSourceDirtinessContext[reason] requiresDOMSelectionExport: boolean }SlateProjectionStore.refresh()返回或发布这个结果editable 侧只保留一个修复桥useProjectionDOMRepairBridge/projection-repair-bridge.ts仅在requiresDOMSelectionExport为真时调度类型化修复requestEditableRepair({ reason: projection-refresh, selection: export-model-to-dom-after-commit, runtimeIds: changedRuntimeIds, })适配器规则随之收紧适配器不得触碰EDITOR_TO_FORCE_RENDER。允许的是source.refresh({ forceInvalidate: true, reason: external })这条规则适用于 legacyEditable.decorate、useSlateDecorationSource、useSlateRangeDecorationSource、annotation 刷新路径以及会改变光标周边可选 DOM 的 widget 刷新路径。该桥由 editable 运行时安装因为只有它能协调 React 提交时序、DOM 修复、IME 状态与选择权导出。公共 DX 保持不变Editable decorate{decorate}继续作为适配兼容存在decorationSources/投影来源是 v2 主路径forceRender、选择权修复标志、投影修复内部细节一律不对外暴露。评审文档还记录了该架构落地的 RED/GREEN 过程在桥接实现之前一等 hook 来源useSlateDecorationSource的浏览器行失败——DOM 光标偏移35对期望41因为第一版的 force-render 只留在 legacy prop 适配层桥接落地后prop 与 hook 两条来源行都把 Slate 选择权与 DOM 光标保持在41。这也解释了为什么 Claim Bar 坚持要覆盖Editable的decorateprop 路径——两条入口共享同一失败模式架构必须统一收敛。验证命令与 Ledger 状态计划文档记录的验证三件套在.tmp/slate-v2checkout 内执行bun lint:fix bun --filter slate-react typecheck PLAYWRIGHT_RETRIES0 bun playwright playwright/integration/examples/decorations-async.test.ts --projectchromium跟进评审追加了契约级测试bun test ./packages/slate-react/test/projections-and-selection-contract.tsx。最终声明为Fixes #5987且跟进架构升级不改变该声明——评审原话#5987 保持Fixes #5987本次工作不改变公共 issue 声明它只是升级该声明背后的架构相关的装饰失效压力#4993、#4997、#3383在各自精确复现通过前不追加修复声明。仓库 ledger 中该 issue 的最终状态可交叉核对issue-coverage-matrix 记录 #5987 为Fixes证明描述为精确的异步Editable.decorate浏览器证明在延迟装饰回调身份变化与 DOM 重排之后保持 Slate 选择权与浏览器 DOM 光标于键入处文档末尾gitcrawl-v2-sync-ledger 将其归入 v2-input-runtime 泳道、状态fixes-claimedgitcrawl-recluster-map 则把证明文件示例site/examples/ts/decorations-async.tsx、测试playwright/integration/examples/decorations-async.test.ts映射到对应簇。注意这些被引用的实现与测试文件位于文档记录的.tmp/slate-v2私有 checkout 路径下本仓库主树不包含它们交叉核对应以 ledger 记录为准。可复用的工程经验这个案例沉淀出的预防规则来自解法文档的 Prevention 一节值得在维护任何模型 DOM 投影型编辑器时借鉴装饰类 bug 的回归必须双轨断言同时断言 Slate 模型选择权与浏览器 DOM 光标位置单轨断言会产生假绿投影刷新改变渲染文本却不产生编辑器提交时投影存储应显式报告这一变化由 editable 运行时负责 DOM 选择权导出适配器不得直接 force-render适配器只刷新来源修复时序归 editable 运行时所有为异步 UI 状态改写文本 DOM的场景保留精确的浏览器测试行——模型级单元测试无法覆盖React 提交后的 DOM 光标落点这一失败面。从更宽的视角看这条修复线也印证了仓库装饰/注解簇文档的核心判断装饰应用改变 DOM 结构与叶子边界选择权映射与合成时序对其极度敏感decorate这类外部时序驱动的 prop是结构性脆弱点把失效信号归位到投影刷新结果、把修复时序归位到 editable 运行时正是把这种脆弱性从回调身份游戏转化为显式契约的做法。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询