Slate v2 装饰与注释(Decorations / Annotations)架构重构:从遗留 decorate 回调走向投影优先的覆盖层系统
2026/9/16 18:29:43 网站建设 项目流程

Slate v2 装饰与注释(Decorations / Annotations)架构重构:从遗留 decorate 回调走向投影优先的覆盖层系统

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

导读

本文基于 plate 仓库中 Slate v2 研究线对 decorations / annotations 问题簇的完整调研结论,系统梳理了旧版 Slatedecorate抽象为何反复制造同一类 bug:因为它同时承担了渲染期标记、跨节点覆盖层、外部驱动的临时高亮和注释型锚点四种职责。读者读完本文后,将掌握旧装饰系统在语义、拓扑、性能、DOM/选区时机与注释锚点五个维度上的失败模式,理解 ProseMirror、Lexical、Tiptap 等编辑器各自的取舍,以及最终推荐的"装饰与注释分离、共享投影管线"的重构方案、React 19.2 运行时姿态与目标 API 形状。

本文以研究计划文档 docs/plans/2026-04-14-slate-v2-decorations-annotations-cluster-research.md 为骨架,其核心产出是 docs/slate-v2/decorations-annotations-cluster.md;文中结论由本地 issue 语料(docs/slate-issues)、Slate 上游 issue#3383、PR#4993/#4997讨论以及仓库源码共同支撑。

为什么装饰与注释值得独立成簇

旧版 Slate 的 issue 语料习惯于把 decorations 摊进更大的主题桶——React 运行时、选区、性能、API 易用性——这掩盖了真实模式。跨主题重新扫描后(见 docs/slate-issues/issue-clusters.md 中明确标注的 cross-cutting seam),发现 decorations 实际在同一时间承担了至少四类不同工作:

  • 文本上的渲染期标记(render-time marks)
  • 跨节点的视觉覆盖层(cross-node visual overlays)
  • 外部驱动的临时高亮(externally-driven transient highlights)
  • 评论与协作光标所需的注释型锚点(annotation-like anchors)

这种职责过载,正是同一族 bug 反复以不同症状出现的原因。结论不是"有一个 bug",而是"有一个糟糕的抽象边界"。旧版decorate回调从语法高亮、搜索命中、AI 建议、远程光标、评论、诊断、占位 UI 到审阅锚点全部包办,这不算"灵活",而是一个垃圾抽象(garbage abstraction),修复方式不是更聪明的回调,而是拆分职责。

核心问题家族

1. 语义坍缩:leaf props 过于有损

最干净的例证是#3383:语义相同但元数据不同的重叠 mark 或 decoration,一旦全部拍平进 leaf props 就无法共存。问题不在于"高亮很难",而在于 leaf 模型只能良好保留正交属性;当两个覆盖层对同一 key 携带不同载荷时,一个胜出、另一个消亡。

相关压力还包括:

  • #2564:marks 与 inlines 在语义上早已混淆;
  • #2465:渲染期 mark 的可用性脆弱,因为渲染器工作在切分后的 leaves 上,而非更丰富的覆盖层模型。

仓库中的 packages/slate/src/interfaces/text.ts 直接印证了这一模型:Text.decorations(node, decorations)的返回类型是{ leaf, position }[]——装饰被切成"叶子片段 + 位置"的扁平行,DecoratedRange只是对上游slate类型的透传。结论:leaf 切分对基础排版够用,但无力保留多个相互独立的覆盖层载荷。

2. 范围拓扑:装饰想跨越多于一个文本 leaf

第二个簇是"形状"问题而非"速度"问题。用户期望装饰能够:跨兄弟节点、桥接 inline 边界、从高阶节点操作、在不改动文档状态的前提下预览或遮罩内容。

相关压力:

  • #4392:跨节点 decorate;
  • #4426:范围遮罩(range masking);
  • #4477:协作写作中锚定选区的评论。

这些都是同一问题的变体:公开的decorate(entry)契约对更丰富的覆盖层行为而言太"文本 leaf 形状",但又太隐式,无法暴露真正的覆盖层/注释模型。一旦覆盖层需要超出"给这段 leaf 片段打标记",该 API 就不再诚实。

3. 性能与失效:装饰传播快速爆炸

这是最热门的运行时簇。本地 issue 文档已标记:

  • #4483:动态 decorations 的重渲染成本;
  • 旧语料中关于嵌套 leaf 失效与大装饰树的笔记。

#4993的关键细节:

  • 在树顶端计算全部装饰再向下传递,会让Range.intersection成为瓶颈;
  • 混合深度树会非常残酷,因为每一层都要用大量装饰集与众多后代做相交运算;
  • 类似"代码容器 + 多行"的嵌套结构会把这变成实际上的不可用。

#4993认为顶层拍平是一次回归,因为它摧毁了旧的"只重装饰树中发生变化的部分"的行为。

#4997的关键细节:

  • selector/store 风格订阅可以比纯 context 传播更好地局部化重渲染;
  • 这在decorate函数本身频繁变化时有帮助;
  • 但它仍然把decorate放在一个脆弱的位置:一个必须与 Slate 协调(reconciliation)和 DOM 选区修复在时间上完美对齐的 prop。

结论:这里的性能痛点不是泛泛的"装饰很慢",而是失效模型之痛(invalidation-model pain)。

4. 选区、IME 与异步时机:装饰后的 DOM 与编辑器状态相互漂移

这是装饰债从"恼人"升级为"破坏编辑"的地方。

相关压力:

  • #3309:装饰后的文本无法被选中;
  • #3162:decorate + IME 输入失步;
  • #4712:带text字段的装饰范围干扰选区;
  • #5987:异步 decorate 更新落地时光标跳动;
  • #4581:删除 decoration/void 后输入在 Firefox 中可能崩溃。

这一族问题反复表达同一件事:

  • 装饰应用改变了 DOM 结构或 leaf 边界;
  • 选区映射与组合(composition)时机对此极其敏感;
  • 外部定时的装饰更新让问题更糟。

#4997是最有价值的线程,因为它没有停在"性能似乎变好了"。它发现了更硬的失败模式:selector 风格订阅模型看起来很有希望,但随后一个真实的 debounced 装饰复现产生了光标跳动、DOM 中出现幽灵纯文本、内部状态损坏。作者的结论很直白:当装饰变化由外部驱动、且不与编辑器onChange同步时,decorate作为 prop 就是一座脆弱的纸牌屋。运行时契约——装饰、选区协调、外部驱动更新三者之间——在结构上就是脆弱的。

5. 注释压力:评论与光标不只是"更多装饰"

旧讨论反复汇聚到同一想法:从文档结构派生的装饰是一回事,外部维护的锚定覆盖层是另一回事。

  • #4477要求注释锚点;
  • #4993的讨论明确回溯更早的 annotation 概念,并指出装饰并不适合做光标;
  • API 相关评论提到 keyed overlays、range refs 与命令式维护的装饰类实体。

这就是注释压力,而非单纯的装饰压力。有用且必须区分的定义:

  • decoration(装饰):从节点内容或局部结构派生的投影(derived projection);
  • annotation(注释):随时间锚定到某个范围上的持久或外部拥有的锚点。

一旦强制二者走同一个decorate漏斗,就会得到:失效语义模糊、稳定/不稳定 decorate 引用的压力、为协作光标或评论而打的补丁、外部状态变化需要重新广播进树时的时机 bug。注释需要显式的所有权与生命周期语义,装饰不会免费给出这些。

#4993#4997实际教会了什么

#4993:契约本就模糊

#4993的真正论点不只是性能,而是契约歧义。存在两种互不兼容的预期:

  1. 旧 Slate 式预期:节点变化 → 局部重装饰;decorate函数引用变化 → 全量重装饰;
  2. Plate / slate-yjs 时代预期:稳定的decorate函数仍应反映变化的外部状态。

这才是真正的断裂线。#4993声称强制整棵树顶层重算代价过高,且破坏了高效的局部装饰传播;同时它也暴露了下游库的合理抱怨:如果 Slate 期望decorate按引用失效,那么该契约从未被明确说明,且对"天然保持稳定函数、变化外部状态"的框架是敌意的。

#4997:更快的订阅并不能修复语义错配

#4997尝试了聪明方案:用 subscription/selector 取代原始 context 搅动,只重渲染装饰切片真正变化了的节点。这改善了纯性能故事,但异步/防抖装饰更新立刻击穿了它。这个结果很有价值,因为它证明问题比"用错了重渲染原语"更深——更好的订阅机制解决不了外部拥有的装饰时机、选区协调风险、藏在decorate里的注释类覆盖层语义。#4997有用不是因为合并了,而是因为它撞到了墙。

当前架构判断:slate-v2 的方向更接近真相

研究结论是明确的:

  • 不要随便复活遗留decorate语义;
  • 不要假装评论/光标/锚点能被更好的 leaf 装饰管道解决;
  • 不要把投影局部装饰和注释锚点再次折叠进一个无差别 API。

当前slate-v2方向更符合证据:投影局部装饰行为保持狭窄、注释锚点得到显式对待、引擎不会为了讨好遗留decorate的怪癖而被加宽。对应的执行权威文档是 docs/slate-v2/decoration-roadmap.md,它把装饰车道锁定为"显式、高性能、React 原生"的覆盖层架构,并明确指出下一阶段重点是 source-scoped(按来源作用域)失效,而不是架构命名或示例。

本地 slate-v2 已有的正确直觉

好消息是 slate-v2 已经拥有大部分正确直觉,只是散落在过多文档中。

1. 投影切片(projection slices)已经胜过第二个装饰模型

最重要的本地规则已经写下来:

  • projection proof 必须把 range 语义与 React overlay store 分开;
  • editable text 应把 leaves 与 projection slices 分开。

这是正确的接缝:Core 拥有逻辑 range 含义,React 拥有订阅广度与切片投递,渲染器消费切片而不是再次发明装饰。

2. 持久锚点需要 range refs / bookmarks,而非回调技巧

注释工作已经找到了诚实的底层设施:

  • range refs 必须事务感知且默认向内(inward)。

评论、审阅锚点、持久诊断等持久跨度不是"最新一次 decorate 函数返回了什么"。它们需要 id、生命周期、亲和(affinity)语义、事务感知的 rebase、提交时发布。这正是 bookmark/range-ref 领域。仓库中的 packages/slate/src/interfaces/location-ref.ts 印证了底层设施:RangeRefaffinity类型已支持'backward' | 'forward' | 'inward' | 'outward' | nullcurrent可随操作变换后随时读取——这是事务感知锚点的直接实现证据。

3. 装饰文本改变了 DOM 契约,桥接层必须承认

本地浏览器证明已经杀死了朴素假设:

  • 装饰后的多 leaf 文本需要累计偏移映射;
  • 装饰后的剪贴板与选中文本助手应剥离仅渲染包装与 FEFF。

重写不能止步于"React 少渲染"。它还必须保持诚实的 Slate 偏移 ↔ DOM 偏移映射、忽略仅渲染包装的剪贴板语义、忽略 FEFF 或占位符垃圾的选中文本语义。配套证据还有 异步 decorate 刷新必须导出 DOM 选区,以及代码块场景下的 语言切换必须触发 redecorate、格式变化必须重建代码行。

4. 巨型文档要 corridor + occlusion,而非虚假的基础分块

本地巨型文档姿态已优于旧 Slate 思维,相关参考见 docs/slate-v2/references/chunking-review.md 与 docs/slate-v2/references/replacement-family-ledger.md。正确默认值是:selector-first 运行时、活跃编辑走廊(corridor)、走廊外遮蔽(occlusion)、重型覆盖层与文本树重渲染分离。

跨编辑器扫描

多数编辑器设计文档在这里变弱:要么盲目崇拜 ProseMirror,要么从五个仓库各挑几个漂亮术语就自称战略。诚实的取舍更窄。

ProseMirror

值得偷师:

  • decoration.ts 中显式的装饰种类与映射纪律:inline decorations、widget decorations、node decorations、跨事务映射、分层DecorationSet
  • selection.ts 与 history 中严肃的选区 bookmark 处理。

需要拒绝的:让 PM 装饰引擎成为所有覆盖层用例的最终真理。原因:它擅长映射的文档附着覆盖层,但在产品需要更丰富的重叠审阅/建议 UI 时偏弱——Tiptap 自己的文档都承认重叠建议被 ProseMirror 装饰限制挡住了。所以偷的是"显式覆盖层类型、映射集合、bookmarks",而不是"所有东西都用同一个装饰引擎"。

Lexical

值得偷师:

  • useReactDecorators.tsx 中通过useSyncExternalStore实现的 React 侧装饰订阅;
  • 显式的节点装饰表面LexicalDecoratorNode
  • LexicalOnChangePlugin中 dirty-set 感知的更新过滤;
  • useYjsCollaboration中带独立光标容器的光标覆盖层分离。

需要拒绝的:假装DecoratorNode是通用文本装饰答案。Lexical 装饰擅长节点级 React 门户与嵌入式 UI,但不是通用重叠 inline 装饰 + 注释锚点底层。偷的是门户/widget 层、订阅/store 纪律、dirty-tag 过滤、文本 leaf 模型之外的协作光标 UI,而不是"让每个覆盖层都成为 decorator node"。

Tiptap

值得偷师:残酷的产品打包区分——comments 作为独立功能系统、mark views 作为编辑器内显式渲染表面。需要拒绝的:依赖 ProseMirror 装饰限制来对待审阅/建议 UX——其 AI 建议文档明确说重叠建议因 ProseMirror 装饰限制无法显示。这对 Tiptap 的产品面没问题,但不是全新重写的正确北极星。偷的是"注释是真正的注释产品,而不是可爱的高亮"与"mark views 与输出序列化分离",拒绝的是把"重叠覆盖层不可用"当成引擎法则。

Premirror + Pretext

值得偷师:snapshot → measure → compose → viewport 拆分、把布局作为独立确定性模型、为页面 chrome 与诊断使用显式 widget 装饰、文档位置与组合布局之间的选区投影与映射。需要拒绝的:把页面布局测量拖进核心编辑热路径。这条车道对分页、页面 chrome、屏外规划与未来虚拟化是金子,但对普通 inline 装饰语义是过度设计。偷的是"布局是派生状态、布局有自己的失效与剖析模型、覆盖层可以从组合输出投影而不拥有文档语义"。

use-editablerich-textareaedix

它们主要让你对这个空间的下限保持诚实。它们证明了:contenteditable 表面需要突变回滚与选区恢复;textarea + 背景覆盖层非常适合纯文本装饰、自动补全、菜单与 IME 安全高亮;小声明式 contenteditable 状态管理器在模型简单、选区快照显式时可行。都不适合作为带持久锚点的结构化富文本引擎的脊柱,值得偷的是小表面教训、显式选区快照、DOM 回滚偏执与 IME 尊重。

TanStack DB

这不是编辑器仓库,这正是它有用的原因。值得偷师:规范化集合心智(normalized collections)、useLiveQuery.ts 通过useSyncExternalStore的 live-query 订阅设计、仅在版本或集合身份变化时重建稳定快照。这是注释与覆盖层索引远优于又一个临时 React context 堆的心智模型。偷"规范化注释集合、可见范围/选中线程/块局部覆盖层的 live queries",不偷"面向普通编辑器消费者的数据库形状公共 API"。

EditContext

这是未来平台压力,不是当下交付指南。概念上值得偷师:显式共享文本缓冲、显式updateSelection(...)、显式updateLayout(...)、通过textformatupdate的显式 IME 装饰请求、以及"外部更新在模型/布局通道诚实时不必然取消组合"的性质——这正是旧 Slate 从未拥有的方向。不偷"今天就硬依赖 EditContext",只偷架构形状。

候选地图其余部分

docs/analysis/editor-architecture-candidates.md 中其余条目仍然重要,只是对本重写而言不那么直接:

  • VS Code + LSP:偷服务边界——diagnostics、语义分析、code actions、线程/审阅智能可以活在核心编辑器引擎之外,以注释或诊断源的身份重新进入;不偷桌面应用级渲染模型。
  • urql:偷可组合源管道、缓存与派生层、可扩展更新流的心智,作为多个覆盖层源组合而不变成单体 Monolith 的灵感。
  • Open UI / 更丰富的文本字段:当作平台压力而非实现指导。平台本身仍缺乏对更丰富文本字段的一致答案,任何严肃编辑器架构都仍需显式处理文本缓冲所有权、选区所有权、绘制期覆盖层与 IME 交互。

短清单指向同一方向:Tiptap 的产品打包、ProseMirror 的严格映射覆盖层语义、Lexical 的 React/运行时订阅、Premirror/Pretext 的布局分离、use-editable/rich-textarea/edix的轻量小表面教训、TanStack DB 的规范化响应式索引、VS Code/LSP 的服务边界、EditContext/Open UI 的未来平台形状。

深入一层的黄金洞见

1. ProseMirror 真正的赢点是子作用域传播,而不只是DecorationSet

decoration.ts 中最强的点不是"它有装饰",而是forChild(...)只把相关的相交 inline 装饰加上子拥有的子树集合交给每个子节点。这与旧 Slate 最糟的行为完全相反:没有贯穿整棵树的顶层扁平装饰列表,而是子局部重叠切片与分层覆盖层所有权。这个具体想法值得偷。

2. ProseMirror 的 bookmarks 正是旧 Slate 缺失的持久性界线

bookmarks 是独立于文档的映射选区:无需挂载 DOM 即可映射、之后对当前文档解析、保存的是持久表示而非陈旧的已解析句柄。这是持久锚点的正确心智模型,也是注释应当骑乘 bookmark/range-ref 语义的原因。

3. Lexical 已经把 Slate 一直揉在一起的三个职责拆开了

  • MarkNode是带 id 的 inline 包装,可横跨文本、inline 元素甚至 inline 装饰节点;
  • DecoratorNode面向节点级渲染 UI;
  • useYjsCollaboration把远程光标 UI 放在独立 DOM 容器。

胜出架构不是"一个覆盖层系统",而是:inline 身份承载包装、锚定 widget/portal 节点、以及适当的全外部覆盖层 chrome。

4. Lexical 的 dirty sets 与 tags 是正确的失效词汇

失效应该以 dirty leaves、dirty elements 与 tags 来表述,而不是"某个回调变了,祝你好运"。对 Slate v2 覆盖层意味着:事务元数据携带失效提示、覆盖层源声明什么会使自己变脏、React 订阅消费狭窄的失效作用域。

5. Tiptap 无意中证明了评论与建议应该保持分离

Tiptap 文档同时说了两件有趣的事:comments 支持重叠线程与丰富产品行为;而作为 ProseMirror 装饰渲染的 AI 建议因装饰引擎限制无法重叠。如果系统需要持久重叠、工作流、线程元数据、程序化 CRUD,它要的是注释语义;如果只需要临时预览、样式、类似 diff 的视觉投影,它往往可以活在装饰语义里。试图用一个机制强行承载两者,正是编辑器 API 变蠢的地方。

6. VS Code 证明严肃编辑器会激进地拆分视觉通道

VS Code 的教训不是"抄 Monaco",而是:diagnostics 变成 inline class 装饰、overview ruler 标记、minimap 标记、sticky 策略与 z-index 的组合;ghost text 用 injected text 做 inline 预览、用 view zones 做附加行;injected text 拥有自己的光标停止语义。即使是成熟文本编辑器也不信任一个"range decoration"原语包办一切,而是拆成 inline 插入文本、行/块附加 zone、ruler/minimap/gutter 的带外诊断通道。这几乎完美映射到提议的 Slate v2 拆分:文本装饰、widgets/chrome、带外诊断通道。

7. Premirror 的失效计划纪律是巨型文档的超能力

真正的好东西不是分页本身,而是坚持显式失效范围、双向位置映射、每事务剖析计数器、按区域增量重组。严肃的覆盖层运行时在巨型文档上就应这样思考:知道什么变了、哪些范围或语义岛屿受影响、哪些视口/布局区域需要重算、度量成本。

8. EditContext 暴露了一个缺失的覆盖层车道:IME 拥有的格式化

IME 组合格式化是自己的通道——textformatupdate不是评论、不是语法高亮、也不是普通搜索高亮,而是平台驱动的瞬态输入法视觉状态。面向未来的设计应为平台/IME 覆盖层车道留出独立优先级与生命周期规则,而不是假装它们是普通注释。

React 19.2 运行时姿态

React 19.2 不会神奇地解决编辑器架构,但确实让正确形状更清晰。

1.useSyncExternalStore应作为覆盖层订阅骨干

这是 React 面向覆盖层状态的明确赢家:它是官方订阅 hook,匹配本地投影 store 方向,也适合规范化注释/装饰索引。文档中的重要警告:getSnapshot必须返回不可变的缓存快照;subscribe身份变化会导致重新订阅;若外部 store 在 Transition 期间变化,React 可能重启并把该更新当作阻塞应用;不鼓励从外部 store 值挂起。由此得到一条硬规则:

活跃编辑走廊不能依赖懒加载/挂起的覆盖层 store 读取或不稳定快照。

2.startTransition只用于非紧急覆盖层工作

Transition 更新不阻塞、可被中断、不能控制文本输入。因此输入、DOM 选区同步、光标移动、IME 组合处理、光标附近的紧急可见覆盖层变化必须留在 transition 之外。transition 的好用途:重建屏外覆盖层索引、重算侧边栏模型、过滤线程列表、加载/重投影不可见页面、昂贵的诊断面板。

3.useDeferredValue用于滞后视图,而非编辑器真理

适用于陈旧但可用的 UI:线程侧边栏、搜索结果列表、diff/建议面板、minimap、inspector 面板。绝不用于:光标周围的真实活动文本装饰、已提交的选区真理、DOM 桥映射。

4.useEffectEvent完美适配带最新配置的桥接监听器

适用于从 effects 触发、需要最新 props/state、但不应让 effect 本身重新订阅的逻辑:selectionchange 监听回调、resize/scroll/layout 桥接回调、编辑器订阅周围的 analytics/logging hooks、绑定编辑器状态的副作用通知。不要滥用它作为真实依赖的逃生舱。

5.<Activity>是巨型文档与侧边栏工具,不是编辑原语

<Activity hidden>在保留状态的同时清理 Effects 并降低隐藏子树的优先级,非常适合注释侧边栏、审阅面板、隐藏页面 chrome 表面、预渲染的下一面板或标签页。但它也意味着隐藏子树的订阅消失了。所以:把真理之源覆盖层 store 放在隐藏 Activity 子树之外;用 Activity 保留 UI 状态而不让所有 Effects 存活;不要把活跃编辑走廊藏在 Activity 里还指望输入健康。

DX 非协商项

如果这次重写真想做好,DX 门槛必须残酷。

API 不能要求用户

  • 靠交换函数身份让装饰刷新;
  • 为正确性构建 WeakMap 缓存;
  • 猜测"稳定回调"是否等于"陈旧输出";
  • 手动把范围扇出到文本 leaves;
  • 为了正确复制文本而理解 DOM 包装泄漏。

API 应当给用户

  • 显式源注册;
  • 显式刷新语义;
  • 显式注释 CRUD;
  • 瞬态与持久覆盖层的明显区分;
  • 局部切片的直白订阅 hooks;
  • 在大型文档上不会拖垮性能的稳定默认值。

如果消费者在能高亮搜索结果之前要先学五个历史 footgun,这个 API 就是坏的。

性能非协商项

必须要有:分层或索引化传播而非扁平自顶向下扫描;来自事务与源刷新的显式失效作用域;块/文本运行时 id 索引;重叠友好载荷存储;活跃走廊优先级;屏外遮蔽与延迟工作;剖析计数器与冻结的基准车道。

绝不能有:按回调身份大范围重渲染;默认全文档重算;摧毁多重性的 leaf-prop 拍平;强制所有覆盖层 UI 穿过文本 leaves;隐藏侧边栏或页面意外地让昂贵订阅保持存活。

何时停止研究

新的一轮研究不再改变架构形状、只会用不同仓库吉祥物复述同一结构时,就该停止——这条线基本已经到了。

已经收敛的:持久锚点需要 bookmark/range-ref 语义;瞬态覆盖层需要投影切片与窄订阅;widget/chrome UI 需要自己的车道;失效必须显式;巨型文档需要 corridor/region 规划而非全树重绘;IME/输入状态是独立严肃子系统。这些已足够开始设计。

进一步研究不太可能改变的:拆分装饰与注释的必要性;widget/chrome 层必要性;显式失效与 range/bookmark 持久性必要性;selector-first React 订阅必要性。

仍未知、但应在设计中而非研究中回答的:精确公共 API 名称;注释是专用 store 还是编辑器拥有的注册表;源刷新作用域如何表达;widget/chrome 条目按 block-key、runtime-id-key 还是两者;遗留decorate的迁移适配器长什么样;哪些车道默认紧急 vs transition/deferred。

推荐做法:本趟之后停止广泛研究,做最后一轮设计——一份书面架构规格、精确类型形状与所有权边界、一两个薄原型、提前冻结基准车道。只有原型暴露矛盾时,才针对该特定矛盾重开研究。

绝对最佳重写方案

短答:两者都做——DecorationsAnnotations——但绝不是"一个 API 两个营销名"。它们共享投影管道,不共享所有权语义。

1. Core 拥有逻辑范围与持久锚点

Core 应拥有:逻辑Range含义;projectRange(editor, range)或等价的纯投影入口;事务感知的 range refs / bookmarks;锚点 rebase 与亲和策略。Core 不应拥有:React 订阅、DOM 绘制覆盖层、视口剔除、页面布局。这让引擎保持 document-first。

2. 装饰是派生的、瞬态的、重叠友好的

装饰应意味着:从已提交快照状态或外部状态派生、瞬态、廉价丢弃与重算、重叠友好、不为持久 id 提供真理。支持语法高亮、搜索命中、诊断、拼写检查式/审阅式临时范围、选区派生高亮投影。不应要求对象拍平 leaf props——逻辑装饰载荷必须保留多重性:两个高亮叠在同一跨度上,系统应持有两个高亮,而不是拍平成唯一胜者。

3. 注释是持久的锚定实体

注释应意味着:稳定 id、元数据、显式生命周期、由 range ref/bookmark 语义支撑的锚点、经事务 rebase、未挂载也可解析。支持评论与线程、远程光标与选区、需要身份与工作流的审阅建议、持久诊断、书签或用户拥有的锚点。这不是可选项——评论不只是"有观点的装饰文本"。

4. 两者都应喂给同一个投影运行时

共享层应:接受逻辑范围或锚点解析;投影为按稳定运行时 id 键控的运行时局部切片;按文本运行时 id、块运行时 id、以及可能更高的语义岛屿 id 索引;为已挂载消费者暴露窄订阅。即"顶部语义分离、底部共享投影/索引管道"。

5. 增加第三层:widgets / portals / chrome

文本切片不够承载一切。还需要一等 widget/chrome 层:评论按钮、选区 affordances、远程光标标签、审阅气球、诊断 popover、页面 chrome 与断页标记。ProseMirror widget 装饰、Lexical decorator portals、Premirror 页面 chrome 都在说同一件事:某些 UI 锚定到文档,但不应建模为 inline 文本样式。因此重写至少应有三个渲染层:文本装饰、持久注释、锚定 widgets/portals/chrome。

6. React 运行时应 selector-first 且索引驱动

不要把覆盖层数组传下树,不要失效巨型 context。要做:useSyncExternalStore或等价订阅语义、稳定快照读取、按运行时 id 订阅、dirty 作用域失效、为聚合视图提供可选派生 selector。本地投影证明与 Lexical 装饰订阅模型在此对齐,TanStack DB 是更好的 store 心智模型。

7. 失效必须显式

decorate的歧义是毒药。新设计应直说:节点变化使局部派生投影失效;显式源刷新使声明的作用域失效;注释变更只重投影受影响的锚点;全文档重算允许但绝不隐式发生。外部状态装饰需要显式刷新路径,而不是"也许稳定函数身份意味着全量刷新"——这个歧义该死。

8. 巨型文档姿态:corridor 优先,虚拟化可选

重写在任何虚拟化幻想之前就应擅长巨型文档。默认姿态:活跃编辑走廊、局部覆盖层订阅、走廊外遮蔽、延迟屏外覆盖层投影、语义岛屿而非盲目子桶。虚拟化随后:锚点活在已挂载 React 节点之上、注释在屏外依然有效、覆盖层索引无需物化整个文档即可回答视口查询、页面布局或屏外规划可消费同一锚点/投影数据。Premirror/Pretext 在此相关,因为它们证明布局可以是派生模型;但它们不是过度复杂化普通编辑的借口。

9. 剪贴板与 DOM 桥契约保持严格

装饰与注释绝不允许再次腐蚀桥接层。保持本地规则:仅渲染包装不泄漏进剪贴板语义;DOM 偏移映射在切分 leaves 上累计;零宽/占位符哨兵不泄漏进选中文本真理;选区协调以已提交语义为准,而非碰巧在场的包装 DOM。如果重写变快了但复制/选中/IME 再次变假,就是失败。

10. 兼容性策略

不要制造decorate(entry) 2.0。做法:为遗留decorate保留窄兼容适配器;把它归类为投影源;为遗留外部状态调用者提供显式刷新 hooks;不把它作为新架构文档中的首选表面。v2 首选表面应谈论:装饰源(decoration sources)、注释 store(annotation stores)、投影运行时(projection runtime)、widget 层——而不是一个魔法回调。

推荐目标形状

CoreEditor.projectRange(editor, range)Editor.projectRanges(editor, ranges)、事务感知rangeRef/bookmark API、注释锚点 rebase 原语。

React 运行时createSlateProjectionStore(editor, options)useTextProjections(runtimeId, layer?)useBlockProjections(runtimeId, layer?)useAnchoredWidgets(runtimeId | blockId)、显式refresh(sourceId, scope?)

Decorations:注册派生源;源返回逻辑范围 + 载荷 + 层 + 优先级;重叠是一等公民。

Annotations:带 id 与元数据的 CRUD store;底层用 bookmarks/range refs;可解析为切片与 widgets。

Widgets:通过 portals 或显式 chrome 层渲染的锚定 UI 条目。

最终建议

若目标是绝对最佳重写:保留投影局部装饰;增加一等注释;让它们成为共享投影管道上的独立系统;增加 widget/chrome 层而非把一切塞进 inline leaves;让失效显式;让订阅索引驱动;让巨型文档策略 corridor-first 而非 chunk-first。这样才能诚实覆盖语法高亮、搜索、诊断、评论、追踪审阅建议、远程光标、持久锚点、巨型文档、未来虚拟化与未来页面布局,而不假装一个回调应该掌管整个该死的东西。

工作簇摘要

遗留装饰系统是一个混合抽象,覆盖渲染期标记、跨节点覆盖层与注释型锚点,并且沿四个轴失败:语义损失、失效成本、DOM/选区时机脆弱性、以及持久锚点缺失所有权语义。

深度阅读与源码锚点

  • 本地 issue 语料: docs/slate-issues/issue-clusters.md(cross-cutting seam 章节)、docs/slate-issues/requirements-from-issues.md、docs/slate-issues/open-issues-ledger.md、docs/slate-issues/open-issues-dossiers/4541-4392.md、docs/slate-issues/open-issues-dossiers/3313-2733.md、docs/slate-issues/open-issues-dossiers/5994-5918.md
  • 上游线索:Slate issue#3383(同语义重叠载荷有损)、PR#4993(契约歧义 + 自顶向下失效爆炸)、PR#4997(selector 订阅改善性能但仍会在异步/外部 redecorate 时机下崩溃)
  • 本地 v2 设计压力: docs/slate-v2/decorations-annotations-cluster.md(本簇完整成文版)、docs/slate-v2/decoration-roadmap.md、docs/slate-v2/references/chunking-review.md、docs/slate-v2/references/replacement-family-ledger.md、docs/slate-v2/references/slate-batch-engine.md、docs/analysis/editor-architecture-candidates.md
  • 解决方案级证据: 投影证明必须拆分 range 语义与 React overlay store、range refs 必须事务感知且默认向内、editable text 拆分 leaves 与 projection slices、装饰后多 leaf 文本需要累计偏移映射、装饰剪贴板与选中文本助手剥离渲染包装与 FEFF、异步 decorate 刷新必须导出 DOM 选区
  • 研究归档: docs/research/sources/editor-architecture/decorations-annotations-overlay-corpus.md(含 ProseMirror/Lexical/Tiptap/Premirror/VS Code 等外部仓库的逐文件锚点)、docs/research/systems/editor-architecture-landscape.md、docs/research/systems/slate-v2-overlay-architecture.md
  • 源码实现证据: packages/slate/src/interfaces/text.ts(Text.decorations返回{ leaf, position }[]切片,印证 leaf-prop 拍平模型)、packages/slate/src/interfaces/location-ref.ts(RangeRefinward/outwardaffinity 与事务变换,印证持久锚点底层)

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询