尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Slate v2 回调记忆化与原生 beforeinput 输入架构:语义命令处理器设计实录

Slate v2 回调记忆化与原生 beforeinput 输入架构:语义命令处理器设计实录 Slate v2 回调记忆化与原生 beforeinput 输入架构语义命令处理器设计实录【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文以仓库中的规划文档 2026-05-14-slate-v2-callback-memoization-dx-ralplan.md 为核心系统讲解 Slate v2当前仓库的下一代编辑器内核如何重构beforeinput原生输入路径把onDOMBeforeInput收敛为「仅限高级场景」的原始原生逃生舱同时为其补充语义化命令上下文并设计onCommand语义命令处理器作为格式化、删除、历史、粘贴等行为的首选通道。读者将掌握 Slate v2 输入运行时的分层架构原生事件 → 输入分类 → 语义命令 → 模型默认 → DOM 修复、EditableDOMBeforeInputContext与EditableInputCommand的完整类型契约、回调身份稳定性stable latest handler的运行时实现思路以及useMemo(() callback)这类记忆化反模式为什么必须被硬切割。一、背景与问题onDOMBeforeInput为什么会成为旧 Slate 的泄漏点1.1 症状浏览器事件拼写成为普通教程在旧版 Slate React 示例中加粗、斜体这类最基础的格式化行为被教成了「手写解析浏览器事件字符串」onDOMBeforeInput{(event) { if (event.inputType formatBold) { event.preventDefault() return toggleMark(editor, bold) } }}这段代码同时暴露出三个问题参见 解决方案文档 的 Symptom 清单用户被迫直面浏览器差异formatBold、formatStrikeThrough这类InputEvent.inputType拼写是浏览器规范层面的细节不应成为应用作者每天要面对的正常 API回调身份泄漏到原生监听器示例为了「稳定」回调身份会在外面包上useMemo(() callback, [editor])导致原生beforeinput监听器的挂载/卸载被应用回调身份绑架所有权模糊原始 DOM 事件处理器把「Slate 运行时的原生输入分类/模型决策」和「应用自己的 mark 模型策略」混在了一起。1.2 深层原因原生输入属于运行时模型行为属于编辑运行时规划文档的 Verdict 一针见血onDOMBeforeInput应当被硬切割hard cut出「普通定制/示例路径」仅保留为原始原生逃生舱raw native escape hatch。理由分两层Slate 哲学Slate 保持不预设立场unopinionated当用户主动要求原生浏览器输入时应当直接暴露原生事件但「不预设立场」不等于「让用户解析浏览器怪癖」——原始 DOM 事件属于边界boundary模型行为属于编辑器运行时runtimeDX 底线普通用户应该写onCommand{(command, { editor }) ...}而不是useMemo(() (event) { switch (event.inputType) { // ... } }, [editor])也不是useCallback((event) { // ... }, [editor])后一种写法只在回调以「订阅/监听器」身份注册、且身份本身就是 API 契约时才成立。二、目标架构语义化编辑流水线规划文档给出了 Slate React 的正常编辑架构共六层native DOM event - Slate React native listener router - input kernel classifies intent/command/selection policy - semantic editable command handlers run - model-owned default applies or native browser path continues - DOM repair / selection export runs这条流水线的关键语义是原生事件归一化、意图/命令分类、DOM 修复由运行时拥有mark 模型策略由应用代码拥有。formatBold、formatItalic、historyUndo、delete、paste、文本插入都不再通过临时拼凑的onDOMBeforeInput示例来教学——那正是「旧 Slate 泄漏」。三、公共 API 目标3.1 原始原生逃生舱onDOMBeforeInput与EditableDOMBeforeInputContextonDOMBeforeInput得以保留是因为 Slate 保持原生友好但契约必须显式化。目标签名onDOMBeforeInput?: ( event: InputEvent, context: EditableDOMBeforeInputContext ) boolean | void契约要点接收原生InputEvent运行时机早于 Slate 的默认模型/原生决策返回true或调用preventDefault()表示该事件已被处理处理器接收上下文对象示例不再需要闭包捕获陈旧的 editor 状态用户代码无需记忆化仅出现在高级文档advanced docs不进入 walkthrough/工具栏教程。目标上下文类型type EditableDOMBeforeInputContext { command: EditableInputCommand | null; data: unknown; editor: ReactEditor; inputType: string; intent: EditableInputIntent | null; native: boolean; selection: Range | null; };3.2 语义命令处理器onCommand与EditableInputCommand为「不依赖 DOM 事件拼写」的编辑器行为新增语义处理器onCommand?: ( command: EditableInputCommand, context: EditableCommandContext ) boolean | EditableRepairRequest | void这是以下行为的统一通道原生格式化命令如formatBold键盘格式化快捷键如modbdelete/backspace/enter 行为来自原生输入或 keydown 的历史 undo/redopaste/drop/yank 路由必须驻留在 Slate React 中的轻量 markdown 示例。目标命令联合类型type EditableInputCommand | { kind: format; format: bold | italic | underline | strikethrough | string } | { kind: history; direction: redo | undo } | { kind: delete; direction: backward | forward; unit?: block | line | word } | { kind: insert-break; variant: open-line | paragraph | soft } | { kind: insert-text; inputType?: string; text: string } | { kind: insert-data; data: DataTransfer } | ...设计上最关键的一点Slate 只报告format命令不替应用决定用bold、strong、fontWeight还是自定义 mark 模型——这保住了 Slate 不预设立场的核心。3.3 示例改写hovering-toolbar的 before/after现状应停止的写法onDOMBeforeInput{(event) { if (event.inputType formatBold) { event.preventDefault() return toggleMark(editor, bold) } }}目标写法Editable onCommand{(command, { editor }) { if (command.kind ! format) { return; } switch (command.format) { case bold: case italic: case underline: editor.update((tx) { tx.marks.toggle(command.format); }); return true; } }} /如果执行轮次暂不引入onCommand兜底方案是onDOMBeforeInput(event, context)——绝不是「仅闭包事件 prop」更不是useMemo(() callback)。四、为什么这是最佳形态4.1 Slate 哲学原生事件属于边界模型行为属于编辑运行时。两者分层后原始 Slate 既保留原生访问能力又不必让常见示例去解析 DOM 事件字符串。4.2 DX用户应该写onCommand{(command, { editor }) ...}而不是用useMemo/useCallback包裹事件回调来维持监听器稳定——除非该回调确实以订阅/监听器身份注册身份本身就是 API 契约。4.3 性能原生监听器应在每个根元素/编辑器根切换时只挂载一次。应用回调变化时运行时应当更新「读取的最新回调」而不是卸载并重新挂载原生beforeinput。实现目标若仓库/代码规范/运行时约束允许使用 ReactuseEffectEvent否则在slate-react内部维护一个稳定的 latest-event helper消费端零记忆化仪式零依赖数组谎言no dependency-array lies。4.4 先前 Slate v2 经验本方案遵循既有的 runtime-owner 教训热编辑策略应属于具名的 Slate React 运行时门面facades而非宽泛的 React 根闭包所有权切割需要静态清单而不只是更小的文件真实输入/选区行为必须经过浏览器证明browser proof归因 React 渲染前应把原生输入通道与直接模型输入分开剖析性能热浏览器路径应保持私有原始决策廉价并暴露模型状态而非原始 DOM 策略理由。五、候选方案对比借鉴什么、拒绝什么规划文档对六个方向的竞品/参考实现做了证据化对比候选证据要点借鉴Steal拒绝RejectProseMirrorEditorProps.handleDOMEvents是底层逃生舱返回 true 需自行preventDefaulthandleTextInput是带from/to/text和默认事务的语义文本插入钩子keymap/inputrules是插件而非原始 DOM 事件 propprosemirror-view的beforeinput保守多为 Android/选区管道原始 DOM 处理器与语义编辑器行为分离给处理器传 view/editor 上下文默认事务/命令集中管理不复制 ProseMirror 的 position 体系或插件复杂度作为 Slate 常规 DXLexical根事件由 editor root 注册安装核心beforeinput把formatBold/formatItalic/formatUnderline、undo/redo、insert/delete、粘贴类输入映射为命令公共扩展行为是命令注册而非onDOMBeforeInputReactContentEditable基本只设置根元素原生输入属于运行时语义命令划分优于用户态 DOM 解析最新行为应通过 editor/runtime 上下文触达而非 React prop 身份抖动不复制 class nodes、dollar helpers 或「命令即整个应用 API」Tiptap扩展暴露addCommands、addKeyboardShortcuts、addInputRules和 paste rules键盘/输入行为在扩展边界被产品化产品级 DX 应像行为注册而非 DOM 探索Plate 应拥有丰富的规则族与打磨的扩展体验原始 Slate 不应变成 Tiptap 那种预设立场的扩展产品edix明确以原生beforeinput驱动要求InputEvent.getTargetRanges对 beforeinput 一律 preventDefault忽略原生 format/history 输入类型用自研轻量事务路径管理输入原生beforeinput可以作为浏览器输入的主信号根监听器生命周期可以简单直接仅忽略format*与 history 对 Slate React 不够这是小表面积架构不是完整文档编辑器答案use-editable / rich-textarea / markdown-editor小型编辑器用直接监听器、mutation observer 或捕获的 React 事件API 刻意小巧、面向特定表面低仪式感很重要小表面不应要求完整的扩展/运行时心智模型不以 mutation-observer-first 包装或 React capture 处理器作为 Slate v2 严肃输入运行时的基础Pretext / Premirror并非直接的beforeinputAPI 指南其教训是布局/测量值得拥有独立的确定性通道而非让输入变得布局感知布局/测量应独立成道不因此让输入变得依赖布局六、当前 Slate v2 状态已有构件与缺口6.1 已经指向正确方向的实时源码规划文档列出现有运行时就绪的部分EditableInputRuleContext已传递editor、event、inputType、selectionEditableKeyDownHandler已使用(event, context)形态EditableCommand已建模 delete、history、insert text/data、selection、set block、toggle mark编辑内核已对beforeinput做意图分类runtime-before-input-events.ts已把原生beforeinput路由到「分类 → 选区同步 → 输入规则 → 模型所属操作 → DOM 修复」。6.2 当前缺口与仓库源码对照仓库当前版本的 EditableProps.ts 中onDOMBeforeInput类型仍是onDOMBeforeInput?: (event: InputEvent) void;对应规划文档列出的缺口onDOMBeforeInput类型仍为(event: InputEvent) void未包含boolean | void与上下文运行时已接受boolean | void但文档/类型没有跟上回调身份仍流经原生监听器/ref 依赖format*类 beforeinput 已被分类为format意图但尚无语义命令承接——这正是示例回退到裸event.inputType的原因inputRules作为Editableprop 对原始 Slate 过于贴近 Tiptap/Plate 产品 API仅当明确限定为底层输入钩子时才保留否则富规则上移给 Plate。仓库 DOMHandlers.ts 中还同时存在onBeforeInput、onBeforeInputCapture与onDOMBeforeInput三条 DOM 处理器通道规划文档明确要求文档不得再把onBeforeInput与onDOMBeforeInput表述为可互换。七、硬切割清单Cut切割useMemo(() callback)回调工厂教用户在 bold/italic/underline 上使用onDOMBeforeInput的文档/示例把onBeforeInput与onDOMBeforeInput表述为可互换的文档由应用回调身份引起的根监听器抖动root listener churn任何暗示「Slate 示例要干净就必须依赖 React Compiler」的表述。Keep保留onDOMBeforeInput作为原生逃生舱onKeyDown作为常规 React 键盘事件逃生舱模型所有的 delete/history/insert/paste 行为留在 Slate React 运行时Plate 作为丰富输入规则/插件家族的归宿。Revise修订onDOMBeforeInput类型扩展为boolean | void并携带上下文EditableCommand纳入原生 format 命令示例优先使用语义命令或常规工具栏onClick文档把原始 DOM beforeinput 移至高级章节。八、实施计划五个阶段与验证门Phase 1契约测试先行TDD类型测试onDOMBeforeInput接受(event, context) boolean | void运行时测试更换onDOMBeforeInput身份时调用最新处理器且不重挂原生beforeinput运行时测试formatBold/formatItalic/formatUnderlinebeforeinput 分类为语义 format 命令运行时测试若onCommand处理了 format 命令Slate 阻止原生默认且不继续模型/原生兜底运行时测试若无语义处理器处理 format 命令Slate 保持安全 no-op/原生拒绝路径不做硬编码的boldmark 变更。Phase 2运行时边界在slate-react内部实现 stable latest-event原生监听器挂载以根元素/编辑器根切换为键把EditableDOMBeforeInputContext注入onDOMBeforeInput在意图/命令分类之后、默认模型/原生应用之前把onCommand注入输入运行时。Phase 3命令/意图形态为原生 format 输入新增format编辑命令归一化原生输入类型formatBold - { kind: format, format: bold } formatItalic - { kind: format, format: italic } formatUnderline - { kind: format, format: underline } formatStrikeThrough- { kind: format, format: strikethrough }不为原始 Slate 硬编码默认 mark 变更命令元数据保留在 editable 运行时内不进入根slate公共导出。Phase 4公共示例与文档重写hovering-toolbar原生格式化走onCommandUI 按钮走常规工具栏onClick移除useMemo回调工厂的导入与使用审查markdown-shortcuts、tables、mentions、iframe仅在真实订阅/配置身份处保留记忆化运行时证明后移除简单Editable事件 prop 上的仪式把onDOMBeforeInput文档移入高级原生逃生舱指引文档中不写迁移/changelog 语言。Phase 5验证从.tmp/slate-v2运行新处理器类型的红/绿类型测试slate-react编辑内核/运行时聚焦测试hovering-toolbar 原生格式化的浏览器聚焦证明markdown 快捷键输入行为的浏览器聚焦证明bun --filter slate-react typecheckbun lint:fixbun check。grep 门禁rg -n -U useMemo\\(\\s*\\n\\s*\\(\\)\\s*\\s*\\([^)]*\\)\\s* site docs packages -g !site/out/** rg -n onDOMBeforeInput.*format|formatBold|formatItalic|formatUnderline site docs packages -g !site/out/**九、问题台账与评审门规划文档对关联 Issue 采用「仅缓存优先的关联记录不做修复声明」的纪律#4681原始onDOMBeforeInput行为保持关联#3568/#3586beforeinput 格式化/mark 变更缺陷在语义 format 命令证明后获得更好覆盖但精确关闭仍需匹配的复现证明#5181陈旧回调/editor prop 压力由 stable latest handlers 处理若落地#4317渲染回调抖动相邻但不由此计划关闭。台账同步目标pr-description.md 新增原生命令边界章节而不改动已修复声明issue-coverage-matrix.md 将上述五个 Issue 刷新为 related/非关闭行fork-issue-dossier.md 记录该表面的缓存优先关联 Issue 轮次。评审门覆盖slate-ralplan仅规划不直接改 Slate v2 源码、hard-cut切割的是示例教学路径而非逃生舱本身、repo-research-analyst竞品对比基于本地源码阅读、performance-oracle主要风险是原生监听器抖动与回调大规模失效、react-useeffect原生监听器生命周期是 effect/订阅问题用户格式化响应是命令/事件处理器逻辑、tdd先锁定回调稳定性/上下文形状/format 命令分类再清理、ce-compound可复用的语义命令/原生 beforeinput 边界模式在验证后沉淀等。十、落地结果与后续收敛onCommand的最终命运规划文档评分为 1.00执行证明补全了剩余 0.05并明确「最终 API 命名为onCommand因为Editable已提供作用域导出的EditableCommand*类型让命令族保持显式」。但需要说明后续的真实收敛在 pr-description.md 的 Accepted current shape 中公共onCommand与EditableCommand*最终被裁掉——因为其语言过于产品化crossed into product command languageonDOMBeforeInput保留为接收 Slate 上下文的原始原生InputEvent逃生舱原生 format 行为归 Slate 所有保持为内部运行时行为由聚焦契约覆盖原生输入监听器在根上挂载一次无需用户useMemo/useCallback即读取最新处理器 propSlate 报告 format 命令但不硬编码 mark 模式pr-description.md。与此同时配套的 example-memoization-hard-cut-ralplan.md 进一步确认三条库级记忆化泄漏渲染文档仍在教renderElement/renderLeaf/renderText/renderSegment用useCallback编辑行为示例仍依赖回调 props 与记忆化规则数组option-object API 迫使示例记忆化对象以避免身份抖动——其中渲染 props 的计划演进方向是editor.extend(editableRenderers({...}))的渲染器注册能力而非继续传授 React 仪式。另一个实战佐证来自 集成测试失败记录当onDOMBeforeInput处理器返回真值时必须先阻止原生默认再报告事件已处理否则浏览器仍会执行原生默认插入——这正解释了boolean | void返回值契约与preventDefault的配套关系。十一、结论Slate v2 的这次回调记忆化与输入架构重构核心结论可以浓缩为三句话原生输入属于运行时beforeinput的分类、命令化与 DOM 修复由 Slate React 内部拥有原生监听器根级挂载一次、通过 stable latest handler 读取最新应用回调用户零记忆化仪式语义命令属于编辑器行为通道format*、历史、删除、粘贴等行为通过语义命令承载应用只回答「mark 模型策略」Slate 不替应用硬编码bold逃生舱保留但降级onDOMBeforeInput仍是高级场景的原生出口但普通示例与文档只教语义路径或常规工具栏onClickuseMemo(() callback)与「React Compiler 才干净」的表述被硬切割。这套「原始事件在边界、模型行为在运行时」的分层纪律既保住了 Slate 不预设立场的哲学又让应用作者摆脱浏览器怪癖与记忆化仪式可作为富文本编辑器输入层设计的一份可复用参考。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表