
Slate v2 示例 DX 重构指南从遗留示例反推六大公共 API 表面【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本篇技术指南以 Slate v2 Legacy Example DX Ralplan 为骨架系统拆解如何在不污染原始 Slate 无意见unopinionated内核的前提下通过修复公共 React 辅助层来消除示例中的样板代码。文章覆盖六个已评审的 API 表面编辑器历史初始化、清单回退行为、代码高亮装饰生命周期、TypeScript 类型推断、注解 store 上下文、void 渲染属性命名并给出可直接照搬的 API 形态、分阶段执行计划与回归证明清单。读完你将掌握如何判断示例里的样板是 API 缺陷而非用户错误、如何用 React 生命周期 Hook 收敛运行时 store 的手工清理、以及如何让 TypeScript 示例教推断而不是教强转。一、评估结论遗留示例正在教坏两种模式该计划给当前状态打分为0.91ready-for-user-review并给出一个尖锐判断遗留示例正在滑向两种坏的教学模式运行时 store 的生命周期样板过多——用户在示例里被迫手写useMemo创建运行时对象再在useEffectcleanup 里手动销毁模型行为被塞进 React 事件处理器——因为扩展/输入 API 还不够好用示例作者只能退而求其次把本应属于编辑器模型的行为写进onKeyDown。计划给出的立场非常明确不要为了让示例好看就让 Slate 变得魔法化opinionated而要修复示例暴露出来的可复用表面。这决定了整篇计划的性质——它更新但不替代已接受的初始化方案 React Editor Initialization And Value Ralplan是一个纯粹的 DX/API 清理 pass。二、意图与边界无意见内核 一等公民示例计划的 Intent 可以浓缩为三点保持原始 Slate 无意见同时让示例有一等公民first-class的体验避免教用户复制那些仅仅因为公共 React 辅助层缺了一层而存在的样板代码对照 Lexical、ProseMirror、Tiptap 比较示例 DX但不引入它们的完整心智模型。范围内范围In scope计划明确圈定了六个待评审表面与执行时的涉及面.tmp/slate-v2/site/examples/ts/**示例源码.tmp/slate-v2/packages/slate-react/src/hooks/use-slate-editor.ts.tmp/slate-v2/packages/slate-react/src/decoration-source.ts.tmp/slate-v2/packages/slate-react/src/hooks/use-slate-annotations.tsx.tmp/slate-v2/packages/slate-react/src/components/slate.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.agents/rules/ralph.mdc规则更新仅执行阶段说明计划中的slate-v2是一个独立的 workspace 检出原文以绝对路径/Users/zbeyens/git/slate-v2/...引用。在当前仓库中对应的 Slate 引擎位于 packages/slate其中slate-react相关的 Hook、decoration source、annotation 上下文等即为该 workspace 的内容本文以计划原文为准并结合仓库内packages/slate源码佐证历史插件与类型契约部分。非目标Non-goals这四条边界决定了所有后续决策不能越界不让slate-react依赖slate-history保持运行时边界干净不给原始 Slate 加 Plate 式插件不复活公开的扩展commands槽位——v2 扩展硬切hard cut明确拒绝该表面不宣称关闭任何 issue——这是 DX/API 清理issue 账本不增加Fixes #....行。三、现场状态Live Current State六个痛点的真实代码计划先逐一定位现状这是所有决策的事实基础。3.1 编辑器创建与历史初始化useSlateEditor创建withReact(createEditor(editorOptions))且只有显式传入时才运行withEditoruse-slate-editor.ts:23CreateEditorOptions目前只携带initialValue与initialSelection两个字段slate-react对slate-history只是devDependency不是运行时依赖——这一点在本仓库的 packages/slate/package.json 中也可印证slate包本身只依赖udecode/utils、is-plain-object、lodash、scroll-into-view-if-needed、slate与slate-dom历史能力由单独的 slate-history/with-history.ts 提供。示例层的历史样板分两种复杂示例重复写withEditor: (editor) withHistory(editor) as CustomEditor如code-highlighting.tsx:54简单示例可以直接传withEditor: withHistory如plaintext.tsx:6。差异的根源是withHistory的泛型保真度不够导致复杂场景必须强转。3.2 清单Checklist回退行为清单目前通过Editable的onKeyDown处理 Backspacecheck-lists.tsx:75该处理器调用模型逻辑并返回trueEditable视其为已处理并阻止默认行为keyboard-input-strategy.ts:74模型行为本身是基于辅助函数的而非扩展拥有check-lists.tsx:97。3.3 代码高亮与装饰源生命周期示例用useMemo创建装饰源并在useEffect中手动destroy()code-highlighting.tsx:60公共底层工厂是createDecorationSource(editor, options)decoration-source.ts:111code-highlighting.tsx导入type Node as SlateNode仅为标注match回调参数code-highlighting.tsx:25——这是典型的为类型擦屁股样板。3.4 注解Annotationstore 上下文Slate目前接受annotationStores并组合它们的投影 storeslate.tsx:62useSlateAnnotations仍要求显式传 store 参数use-slate-annotations.tsx:68collaborative-comments.tsx把同一个 store 传给Slate又手工向下传给CommentListcollaborative-comments.tsx:540——同一份数据被反复透传。3.5 Void 渲染属性renderVoid收到名为target的属性但该值字面上就是一个Patheditable-text-blocks.tsx:456导出的属性类型是target: Patheditable-text-blocks.tsx:496示例随后把该属性类型写为target: Path并当作at使用images.tsx:119。名字叫 target、类型是 Path、用起来是 at——这是命名与语义脱节的典型反例。四、生态证据Lexical / ProseMirror / Tiptap 怎么处理同一问题计划没有闭门造车而是对照了三大编辑器生态的本地源码Lexical核心createEditor默认不包含历史vanilla 示例用registerHistory显式注册lexical/examples/vanilla-js-iframe/src/main.ts:38React 富文本通过HistoryPlugin /插件加入lexical/examples/react-rich/src/App.tsx:159更新的扩展路径在 dependencies 中显式包含HistoryExtensionlexical/examples/extension-vanilla-tailwind/src/main.ts:83清单与 Tab 行为注册为编辑器命令而不是散落在应用onKeyDown分支里checkList.ts:68、TabIndentationExtension.ts:83。ProseMirror历史是一个插件带事务元数据、分组与协作 rebase 行为不是核心编辑器默认能力history/src/history.ts:258。TiptapStarterKit 默认包含 undo/redo但允许用undoRedo: false关闭starter-kit.ts:95、starter-kit.ts:218更底层的扩展显式注册 ProseMirror history 与键盘快捷键undo-redo.ts:46键盘行为通过扩展的addKeyboardShortcuts注册由扩展管理器转成 keymap 插件ExtensionManager.ts:110。结论三条可复制经验复制显式历史 / 意见化预设的拆分——原始引擎与历史插件/扩展分离复制扩展拥有键盘行为——模型行为归扩展React 事件钩子只做逃生舱不要让原始 Slate 静默包含历史——这是对协作、只读、历史分叉与自定义批处理场景的尊重。五、决策简报六个可落地的公共 API 决策这是整篇计划的核心每一条都给出为什么 目标形态 被否决方案。5.1 历史默认不进useSlateEditor决策默认不把历史放进useSlateEditor。理由slate-react运行时并不拥有slate-historyLexical 与 ProseMirror 都让历史保持 opt-inTiptap 只在意见化的 starter kit 里默认历史协作场景常常需要自定义历史隐藏默认会造成意外双历史double-history和迁移痛苦。目标形态const editor useSlateEditor({ initialValue, withEditor: withHistory, });自定义组合时const editor useSlateEditor({ initialValue, withEditor: (editor) withImages(withHistory(editor)), });执行要求修复withHistory的泛型保真使常见示例不再需要as CustomEditor。如果仓库未来创建slate-starter预设包或示例专用辅助层该预设可以默认包含历史并提供显式关闭开关但必须保持远离原始useSlateEditor。被否决const editor useSlateEditor({ initialValue }); // 隐藏历史计划原话That is convenient but wrong for Slate.本仓库中的实现佐证withHistory在 packages/slate/src/slate-history/with-history.ts 中是独立的泛型插件为T extends Editor的编辑器注入history、undo、redo与批处理 APIpackages/slate/type-tests/history.ts 用ts-expect-error断言了history.undos批次必须携带 operations、setSplittingOnce只接受 boolean正是泛型保真 类型契约的落地形态。5.2 清单 Backspace从组件事件回归模型行为决策当前onKeyDown方案机械上安全但不是最佳 DX。为什么会这样v2 的Editable拥有按键分类、组合输入composition、shell/虚拟化修复与默认行为阻止用户按键处理器返回true就接入这条运行时路径。为什么还不够好清单 Backspace 是模型行为不是组件事件关注点应用作者不应每次使用清单都记得接线onKeyDownLexical 与 Tiptap 把这类行为放在命令/扩展里。目标保留当前辅助函数作为本地安全桥为需要按键意图访问的模型行为增加扩展拥有的键盘/输入能力capability通过该能力或类型化的withChecklists组合器实现清单 Backspace而不是在示例里逐个写Editable onKeyDown不复活公开扩展commands——v2 扩展硬切明确拒绝公开commands槽位改用 capabilities 或运行时输入处理器。5.3 代码高亮与装饰源生命周期新增 React 生命周期 Hook决策为装饰源增加 React 生命周期 Hook。现状样板太多手工代码const codeHighlightingSource useMemo( () createDecorationSource(editor, options), [editor], ); useEffect( () () codeHighlightingSource.destroy(), [codeHighlightingSource], );目标形态const codeHighlightingSource useSlateDecorationSource(editor, { id: code-highlighting, dirtiness: [text, node], read: ({ snapshot }) collectCodeProjections(snapshot.children), runtimeScope: ({ snapshot }) collectCodeRuntimeScope(snapshot), });规则createDecorationSource保留为底层 API在slate-react新增useSlateDecorationSource负责常见 React 生命周期不要隐藏dirtiness或runtimeScope——它们就是性能契约invalidation contract通过抽取命名辅助函数削减code-highlighting.tsx的体量。5.4 TypeScript 推断示例要教推断不教强转决策激进清理示例类型。当前最差范例match: (n: SlateNode) Node.isElement(n) n.type ParagraphType;目标形态match: (node) Node.isElement(node) node.type ParagraphType;执行清单删除无用的type Node as SlateNode导入删除 prop/API 已能推断的内联回调参数类型保留真正导出、无处可推断的组件 prop 类型在组合器类型修复后删除可避免的as CustomEditor、as any与别名强转若仍有强转必须附带局部理由把规则写入.agents/rules/ralph.mdc然后运行pnpm install规则是生成技能的源头。需编码的规则文本For TypeScript examples, prefer inference. Do not annotate callback parameters, alias broad node types, or use as any / public type casts unless the compiler cannot infer the public API shape. Prefer type guards, satisfies, and fixed generic surfaces over local assertions.5.5 注解 store 上下文单数annotationStore 上下文默认 Hook决策store 应从 Slate 上下文中消费公共 provider prop 应为单数annotationStore。现状问题Slate接受复数annotationStoresDX 更差。一个SlateAnnotationStore本身就通过allIds/byId存储多个注解channel/source 的区分应落在注解数据/投影上而不是复数的 provider prop。目标形态Slate annotationStore{annotationStore} editor{editor} CommentList / /Slateconst snapshot useSlateAnnotations();API 形态useSlateAnnotations()使用最近的注解 storeuseSlateAnnotations(store)对外部侧边栏、跨编辑器检查器、显式非上下文读取仍然有效若不存在 store按当前 Hook 哲学返回空快照或在开发环境抛出若产品确实需要独立生命周期的多个 store暴露显式组合辅助函数或要求应用自建聚合 store——不要让公共 provider prop 变成复数不要把注解 store 放到核心编辑器上——它们是 React/投影运行时状态不是文档模型状态。5.6renderVoid属性命名target→path决策把公共renderVoidprop 从target改名为path除非值先变成真正的 target 对象。当前 APItarget: Path;这很含糊。如果值就是Pathprop 就应该是path: Path;未来可选形态仅当确实有用target: { path: Path; runtimeId: RuntimeId; }不要把target: Path作为 v2 稳定公共 API——它听起来抽象实际信息量比path更少对 Agent 和人类都不友好。六、分阶段执行计划Phase 1–6与验收标准计划明确要求不要一次性实现所有阶段首次执行应先做 Phase 1 一个窄幅示例清理让类型证据先落地。阶段内容验收标准Phase 1历史与组合器类型useSlateEditor默认保持无历史复查withHistory/withReact泛型移除简单示例中的强转为withHistory保持ValueOfT与编辑器交叉类型增加类型测试示例可用withEditor: withHistory自定义组合器无需强转除非自定义编辑器类型刻意比运行时扩展更宽Phase 2清单行为归属保留onKeyDown作为基线证明设计兼容 v2 输入运行时与组合保护的最小扩展键盘/输入能力把清单 Backspace 移入withChecklists或清单扩展补充清单项开头按 Backspace的聚焦测试清单示例不再在Editable手工接线 BackspaceIME/组合按键行为保持绿色Phase 3装饰 Hook新增useSlateDecorationSource迁移代码高亮、搜索高亮、外部装饰源、markdown 预览、高亮文本等示例保留底层createDecorationSource导出除演示底层 API 的示例外不再手工配对createDecorationSource cleanupuseEffect装饰 store 指标与 runtime-scope 行为不变Phase 4注解上下文 Hook公共 provider prop 从annotationStores改名为annotationStore由Slate annotationStore派生上下文useSlateAnnotations(store?)与useSlateAnnotation(id, store?)支持上下文默认迁移协作评论、审阅评论、持久化注解锚点collaborative-comments.tsx不再为列注解把同一 store 同时塞进Slate和组件 props一个 store 可承载多个注解 channelPhase 5Void prop 改名RenderVoidPropsT[target]改名为path更新消费它的示例与 Hook仅在测试/示例需要迁移桥时才保留临时内部别名公共示例把path传给 transforms 或useElementSelected若未来引入稳定对象必须按真实 target 命名与类型化而非 Path 别名Phase 6示例类型清理与规则同步移除所有示例中无用的回调参数标注与别名导入在 API 修复后移除可避免的强转更新.agents/rules/ralph.mdc的类型推断规则运行pnpm install同步生成的 skillsrg -n match: \(n: SlateNode\)|type Node as SlateNode| as any| as CustomEditor .tmp/slate-v2/site/examples/ts只剩有正当理由的残留.agents/rules/ralph.mdc包含推断规则七、回归证明改完怎么验证实现后必须执行的聚焦证明清单bun --filter slate-react typecheckbun --filter slate-history typecheck聚焦的 Slate React 装饰源生命周期测试聚焦的注解 store Hook 测试聚焦的清单 Backspace 测试聚焦的示例 typecheck 或示例应用 typecheck浏览器冒烟测试页面/examples/check-lists、/examples/code-highlighting、/examples/collaborative-comments、/examples/images、/examples/embeds、/examples/mentions除非聚焦行指向运行时选区/输入风险否则不跑全量浏览器集成。八、维护者异议与回应七个关键 QA计划通过 steelman 记录了对立观点这些问答最能体现设计权衡历史就该默认人人都要 undo。否。人人都要 undo直到协作、只读、历史分叉或自定义批处理出现。Lexical、ProseMirror、Tiptap 都把原始引擎与历史插件/扩展分开Slate 也应如此。但示例没有默认历史更吵了。正确。那就修组合器类型与示例辅助层的故事而不是改包边界。清单行为写在onKeyDown更简单。对单个文件更简单对被人复制的示例更糟。模型行为属于编辑器扩展行为React 事件钩子只是逃生舱。装饰 Hook 会隐藏性能。只有当它隐藏dirtiness和runtimeScope时才会。Hook 应隐藏生命周期清理而非失效契约。注解 store 是外部的Hook 应显式接收 store。显式 store 参数应保留。默认值应使用最近的 Slate 上下文因为Slate annotationStore就是编辑器局部的投影通道。如果我有评论、建议、审阅标记呢放进同一个SlateAnnotationStore加kind/channel/source字段。store 本身就是集合。若确实需要独立生命周期先组合再传给Slate。target听起来面向未来。只要类型还是Path就不成立。用误导性名字做面向未来设计是假设计要么叫path要么做成真正的 target 对象。九、应用技能记录与评分计划明确记录了所使用的技能与验证维度slate-ralplan已应用基于活源码的规划/评审 passintent-boundary-pass意图、范围、非目标、issue 边界均已显式化steelman-pass记录了维护者异议high-risk-deliberate-pass公共 API 与运行时行为变更以证明为准performance/performance-oracle装饰失效与运行时 store 生命周期保持dirtiness与runtimeScope可见react-useeffect用 Hook 收敛用户层重复的useMemo/useEffect清理仪式tdd作为证明要求而非实现方式。六个维度的评分满分 1.0React 19.2 运行时性能0.90、Slate 无意见 DX0.94、Plate 与 slate-yjs 迁移主干0.88、回归证明测试策略0.90、研究证据完整性0.92、shadcn 风格组合性与极简0.92加权总分0.91。十、执行日志Phase 1–6 已全部落地计划的执行日志记录了从决策到证据的完整闭环全部阶段标注为completePhase 1useSlateEditor({ withEditor: withHistory })保持默认无历史且保留ReactEditor HistoryEditor交叉类型清单示例改为直接withEditor: withHistory不再as CustomEditor新增覆盖装饰 sidecar 状态可选的示例形态类型契约。Phase 2新增editableInputRules(...)作为从编辑器扩展能力注册Editable输入行为的 Slate React 辅助函数Editable合并显式 propinputRules与扩展输入规则清单示例安装checklists扩展不再在示例级Editable onKeyDown接线 Backspace。浏览器冒烟在 Slide to the left. 开头按 Backspace复选框数从 6 变 5 且文本保留。Phase 3新增useSlateDecorationSource(editor, options)代码高亮、搜索高亮、markdown 预览、高亮文本、外部装饰源、渲染策略运行时示例全部从手工createDecorationSource cleanupuseEffect迁移底层 API 保留dirtiness/runtimeScope在调用点保持可见。Phase 4Slate annotationStores{[store]}改名Slate annotationStore{store}新增注解 store 上下文与上下文默认的useSlateAnnotations()/useSlateAnnotation(id)保留显式 store 参数供树外/跨编辑器读取rg annotationStores在源码与文档中零命中。Phase 5renderVoid改为接收{ element, path }而非{ element, target }RenderVoidProps[path]是唯一公共 path 字段不保留target别名useElementSelected(path?)的Path参数按字面命名rg验证target相关模式零命中。Phase 6删除type Node as SlateNode、match: (n: SlateNode)、as any、as CustomEditor等禁止模式huge-document的 content-visibility 强转换成窄解析器类型推断规则写入ralph.mdc并用pnpm install同步生成技能新增slate-reactchangeset 记录公开注解/void API 清理。最终验收rg在site/examples/ts零命中。每个阶段都附带了bun test、bunx tsc --project ... --noEmit、bun --filter slate typecheck、bun --filter slate-react typecheck、bun --filter slate-history typecheck、bun typecheck:site、bun lint:fix与dev-browser冒烟证据。十一、仓库源码佐证历史插件的独立性与类型契约计划讨论的显式历史 泛型保真在当前仓库中可以直接找到实现证据with-history.ts 是独立的泛型插件为T extends Editor注入historyredos/undos栈、redo/undo以及tf.withoutSaving、tf.withoutNormalizing、tf.withMerging等批处理 API——历史被设计为可插拔能力而非编辑器内置history.ts 与 history.spec.tsx、with-history.spec.tsx 提供实现与行为测试type-tests/history.ts 用ts-expect-error锁定类型契约历史批次必须携带 operations、setSplittingOnce只接受 boolean——这正是 Phase 1为withHistory保持ValueOfT与编辑器交叉类型增加类型测试的仓库内样板create-editor.ts 中createEditor({ children, selection })只处理文档初值与选区HistoryApi以独立命名空间导入进一步印证核心引擎不内置历史。这些证据表明计划的六个决策并非空中楼阁而是对既有代码结构的顺理成章的收敛——slate-history保持独立、注解 store 留在 React 投影层、void prop 用字面命名、装饰源生命周期由 Hook 接管、示例类型教推断。十二、Ready State面向执行者的启动建议计划以ready-for-user-review状态收尾并给出明确启动策略不要一次性实现所有阶段。第一次执行应取 Phase 1历史与组合器类型加上一个窄幅示例清理让类型证据先落地再触碰其余阶段。执行日志显示该建议已被遵守并全部完成最终状态可置为done。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考