Slate v2 只读表面恢复:在 EditableBlocks 上重建 readOnly 的真实 DOM 契约
发布时间:2026/9/16 22:48:48 作者:尧图编辑部 阅读量:1,286

Slate v2 只读表面恢复在 EditableBlocks 上重建 readOnly 的真实 DOM 契约【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本篇技术指南围绕 Plate 仓库中 Slate v2 ReadOnly Surface Recovery 计划 展开讲解 Slate v2 重写过程中如何把readOnly真正归还给结构化编辑表面EditableBlocks/EditableTextBlocks让应用不再需要手工伪造编辑器根节点来实现只读展示。读完本文你将理解诚实包边界的工程原则、只读表面在 DOM 层面对输入与粘贴的拦截机制以及isReadOnly在编辑器核心中的真实挂载方式。背景Slate v2 重写中的诚实表面原则在 Plate 仓库的 Slate v2 重写路线中团队遵循一条核心原则公开 API 只能承诺已经在真实浏览器中被证明过的行为。这条原则的落地起点是EditableBlocks——它被确立为 v2 第一个公开的编辑器面对表面editor-facing surface。根据 2026-04-04-v2-editable-blocks-can-be-the-first-public-editor-surface.md 的记录EditableBlocks打包了以下经过验证的行为根挂载与 DOM 调和通过Editable顶层文本块渲染零宽策略zero-width policy投影驱动的 leaf 切分projection-driven leaf splitting占位符支持mark 占位符渲染可选的投影 store 接线它刻意保持狭窄只处理顶层元素块、每块一个文本子节点。这种狭窄即特性的设计是为了避免在混合节点与嵌套 inline 故事被证明之前就过早发布一个更宽泛的 API——发布一个更宽的 API 只是给不确定性换了个名字。从 architecture-contract.md 中可以看到当时完成的 release 形态锚定表面是SlateEditableBlockswithHistory(createEditor())而只读read-only正是这些表面需要回答的家族能力之一。问题readOnly 缺位与应用侧的手工伪造根节点在结构化表面尚未拥有readOnly之前应用要做只读展示只能靠手工伪造编辑器根节点——即自己拼接一个假的编辑器外壳来模拟只读效果。这不仅让每个应用重复造轮子更让包的表面契约与真实行为脱节。该计划的 Goal 原文非常明确RestorereadOnlyon the structured editing surface so apps do not need to fake the editor root by hand.也就是说恢复readOnly的目的不是加一个开关而是让结构化编辑表面真正拥有只读语义把如何呈现只读从应用侧收回到包内。失败方案复盘为什么加一个 prop是撒谎在恢复readOnly之前团队其实走过一段弯路这段经验记录在 2026-04-06-v2-read-only-should-stay-app-level-until-the-editor-surface-owns-it.md 中是理解这次恢复的关键上下文。当时最诱人的做法是直接在当前Editable表面上强加一个readOnlyprop然后宣布该家族被覆盖。但验证结果显示这是彻头彻尾的谎言矩阵中read-only一行仍然渲染出contenteditabletrue同一表面仍然暴露roletextbox一个号称只读的包接缝package seam行为上仍然像一个活跃的编辑根节点换句话说DOM 契约没有改变只是加了个 prop 标签。该文档给出的 root cause 判断是当时slate-react的编辑器面对表面仍围绕活跃编辑设计包括选择所有权selection ownership、剪贴板所有权clipboard ownership、活跃的 DOM 调和。如果只读选项仍然暴露活跃编辑的 DOM 契约那么包声称的比它实际拥有的多。这条经验沉淀为一条可复用的工程规则在把某个家族标记为完成之前先证明 DOM 契约prove the DOM contract before calling a new package prop done如果行为只有通过应用组合app composition才变绿就把它记录为 app 级当前表面而不是包级稳定 API不要因为路线图想推进某个家族就保留一个损坏的便利 prop解决方案从 app 级表面到包级表面这次恢复分两个阶段推进阶段一2026-04-06保持 app 级先立起真实表面。当时的选择是保留Editable与EditableBlocks作为活跃编辑表面同时用现有渲染原语构建一个 currentread-only示例表面并在替换矩阵中证明它。这让替换故事保持诚实只读家族被覆盖了但覆盖它的是建立在运行时原语之上的当前表面而不是一个过早稳定的包 prop。阶段二2026-04-09把readOnly恢复进结构化表面。也就是本文主题的这次恢复落地结果包括在EditableBlocks/EditableTextBlocks上恢复readOnly扩大运行时证明runtime proof使结构化表面真正阻止 paste 与 input并暴露正确的只读 hook 状态简化只读示例使用真实的公开表面而不是手工构建的假编辑器根节点其中阻止 paste 与 input和暴露正确的只读 hook 状态是这次恢复的两个关键验收点——前者说明只读不再停留在视觉层后者说明只读状态成为可编程查询的 hook 事实。底层实现印证isReadOnly 与只读 void 语义虽然EditableBlocks/EditableTextBlocks属于slate-react历史包层的渲染表面但只读语义在编辑器核心中留下了明确的实现痕迹可以从当前仓库源码中直接印证。isReadOnly编辑器 API 上的只读查询在 create-editor.ts 中编辑器 API 显式绑定了只读查询isReadOnly: bindFirst(isReadOnly, editor),其实现位于 internal/dom-editor/isReadOnly.ts本质是委托给slate-dom的DOMEditor.isReadOnlyimport { DOMEditor } from slate-dom; import type { Editor } from ../../interfaces/editor; export const isReadOnly (editor: Editor) DOMEditor.isReadOnly(editor as any);这说明只读状态并不是 React 组件里临时算出来的而是编辑器实例上可查询的一等状态first-class statebindFirst把它与编辑器实例绑定任何上层表面包括EditableBlocks都可以通过editor.api.isReadOnly()或等价路径读取。isTargetInsideNonReadonlyVoid只读下 void 语义的守卫与只读语义直接相关的还有 internal/dom-editor/isTargetInsideNonReadonlyVoid.tsexport const isTargetInsideNonReadonlyVoid ( editor: Editor, target: EventTarget | null ) { try { return DOMEditor.isTargetInsideNonReadonlyVoid(editor as any, target); } catch {} return false; };它在 create-editor.ts 中同样被绑定进编辑器 API。这个守卫回答的问题是当前事件目标是否位于一个非只读的 void 节点内部。在只读表面下编辑器需要区分完全不可交互的只读区域与虽然整体只读、但 void 内部仍有自定义交互如按钮、表单控件的区域。这正是只读表面在拦截输入/粘贴时保留正确交互出口的底层依据。渲染 API 恢复的协同同一天完成的 Rendering API Recovery 计划 与只读表面恢复是配套的renderText在EditableText上恢复并经由EditableBlocks/EditableTextBlocks逐层转发同时恢复了自定义占位符宿主renderPlaceholder与leafPosition元数据。这意味着只读表面的渲染管线是完整自洽的——应用既可以只读展示文本也可以为只读模式注入自定义文本宿主与占位符。示例简化用真实公开表面替换手工伪造根恢复前只读示例为了绕过包内readOnly的缺位需要手工伪造一个编辑器根节点来模拟只读效果恢复后示例可以直接使用结构化公开表面并传入只读配置不再需要任何手工拼装的假根。这种简化在替换家族账本replacement-family-ledger.md中也有对应记录Read-only Family 状态为 Preserved保留其当前事实是read-only editorial behavior is current and direct即只读编辑行为由当前表面直接提供不再依赖应用层变通该家族由 legacyread-only与 currentread-only两条示例行共同支撑证据。验证与回归该计划自带的验证命令如下来自原文档yarn workspace slate-react run test—— 运行slate-react包的测试含恢复后新增的只读表面运行时证明yarn test:custom—— 运行自定义测试集合仓库中用于补充常规单测的浏览器/集成证明通道yarn lint:typescript—— TypeScript 类型与 lint 检查确保恢复没有破坏类型契约对照当前仓库packages/slate/package.json 提供testplate-pkg p:test即bun test、typecheck、lint等脚本仓库根 package.json 则有testbun tooling/scripts/test-fast.mjs、lintbiome check . eslint与g:typecheck等聚合入口可以按包粒度或全仓库粒度复跑同类验证。家族矩阵视角功能恢复与源码同构是两回事值得说明的是功能层面的readOnly恢复与示例源码的legacy 同构是两个不同维度。在 example-parity-matrix.md 的 2026-04-15 git-diff 重基线记录中read-only行的源码相似度约为0.30125 行 legacy 对 35 行 current42 行 diff当时仍被标记为open。这意味着表面上只读行为已经恢复且被证明但示例源码与 legacy 版本之间仍存在结构差异例如使用当前EditableBlocks表面替代 legacyEditable以及当前类型/模块语法差异。这正是该重写项目的独特之处它用功能矩阵与源码 diff 矩阵两套账本分别跟踪行为是否正确与源码是否同构避免用源码相似度冒充行为正确性也避免用行为正确性掩盖源码漂移。关键收获只读是表面职责不是应用补丁readOnly恢复进EditableBlocks/EditableTextBlocks后应用无需再手工伪造编辑器根节点。DOM 契约是验收标准一个 prop 只有在 DOM 层真实生效contenteditable、role、输入/粘贴拦截、hook 状态才算完成否则就是包声称的比它拥有的多。只读状态在编辑器核心有真实挂载点isReadOnly经由bindFirst挂到编辑器 API 上并委托DOMEditor.isReadOnlyisTargetInsideNonReadonlyVoid守卫只读表面下的 void 交互出口。行为恢复与源码同构分开记账read-only家族功能已恢复Preserved但示例源码与 legacy 的 diff 同构度仍由矩阵单独跟踪。对需要在 Slate v2 结构表面实现只读展示的开发者本次恢复意味着一条直接的路径使用公开的EditableBlocks/EditableTextBlocks表面并开启readOnly即可获得阻止粘贴/输入、暴露只读 hook 状态的完整只读契约而无需自建假编辑器根节点。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考