ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Plate Slate React Runtime 一次性拆分方案:从 runtime.tsx 行为垃圾场到按行为域划分的测试所有权

Plate Slate React Runtime 一次性拆分方案:从 runtime.tsx 行为垃圾场到按行为域划分的测试所有权 Plate Slate React Runtime 一次性拆分方案从 runtime.tsx 行为垃圾场到按行为域划分的测试所有权【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文基于 Plate 仓库中一份已完成的架构计划docs/plans/2026-04-13-slate-react-runtime-one-shot-breakup-plan.md完整还原如何将 slate-v2 侧packages/slate-react/test/runtime.tsx这个“巨型运行时测试桶”一次性拆分为按行为域behavior domain划分的独立测试所有者proof owner。读完本文你可以掌握一套大型 React 运行时测试文件治理的完整方法论需求边界、目标文件架构、逐条测试迁移清单、编辑纪律、验证命令与验收标准并理解 Plate 团队如何把“挂载桥mounted bridge”“渲染原语renderer primitives”等仓库经验直接落进测试组织方式。背景为什么要对 runtime.tsx 动刀在 Plate 的 slate-v2 演进过程中packages/slate-react/test/runtime.tsx逐渐变成了一个“运行时垃圾场runtime landfill”所有与 React 运行时相关的行为测试——provider hooks、withReact/ReactEditor桥接、渲染原语、挂载的 Editable 行为、投影与 selection、应用侧定制、大文档滚动——都被堆进同一个文件。这种组织方式的直接后果是任何一条 hook 回归都要在数百行无关测试里翻找任何渲染原语改动都可能牵动与它毫无关系的断言未来接手者无法“一次打开一个文件就找到某个 API 回归的归属”。该计划frontmatter 中标注status: completed日期 2026-04-13给出的整改目标只有五条但每一条都是硬约束保持当前绿色行为不回归preserve current green behavior按行为域迁移测试而不是按任意行块切分共享的挂载/测试基础设施收敛到一个可复用 helper 中surface-contract.tsx继续持有 API/公共表面所有权用显式的“行为域 proof owner”取代单一的 runtime 桶。计划文档中引用的文件行数是拆分前的基线注意文档中的绝对路径指向 Plate 开发者的 sibling 仓库slate-v2本仓库不包含该目录此处仅作记录文件拆分前行数职责surface-contract.tsx496API 面向的公共表面契约provider-hooks-contract.tsx560provider 与 hooks 契约react-editor-contract.tsx542withReact/ReactEditor兼容表面primitives-contract.tsx985渲染原语契约editable-behavior.tsx755挂载的可编辑表面行为projections-and-selection-contract.tsx302投影与 selection 契约app-owned-customization.tsx639应用侧定制large-doc-and-scroll.tsx424大文档与滚动runtime-fixtures.ts1174集中式运行时 fixturestest-utils.ts106共享 JSDOM 挂载 helper拆分完成后的真实状态是runtime.tsx已删除provider/hooks、withReact/ReactEditor、原语、挂载 Editable 行为、投影/ref 行为、应用侧定制、大文档各 lane 都有了显式所有者挂载 helper 共享于test-utils.ts运行时 fixtures 集中到runtime-fixtures.ts。指导拆分的两条仓库经验Source Learnings计划明确列出两条来自 Plate 解决方案库solutions的既有经验它们是拆分决策的直接依据ReactEditor与withReact应该行驶在“挂载桥”上而不是伪装成旧版插件栈。对应经验文档2026-04-09-slate-v2-reacteditor-should-ride-the-mounted-bridge-and-keep-base-components-standalone.md。这条经验决定了react-editor-contract.tsx的独立存在——它的域名就是“挂载桥 ReactEditor 兼容表面”。渲染器契约应该迁入稳定的所有者表面而不是永远以“proof 文件复制粘贴”的方式存在。对应经验文档2026-04-04-v2-renderer-primitives-should-own-node-shapes-not-example-markup.md。这条经验决定了primitives-contract.tsx的独立存在——渲染原语应该拥有节点形状node shapes而不是示例标记example markup。值得强调的是这两条经验在 Plate 仓库中是持续被验证的。例如packages/core/src/react/slate-react.ts与 withPlateReact.ts 正是 Plate 主仓库中withReact语义的落点从源码结构看Plate v2 的 React 桥接同样遵循“组件保持独立、桥接走运行时”的思路与计划中“不要伪装旧插件栈”的原则一脉相承。目标文件架构拆分完成后packages/slate-react/test/应当呈现如下结构runtime.tsx不复存在surface-contract.tsxAPI 面向的表面以及已经拆分出去的低层公共行为test-utils.ts只放共享的 JSDOM 挂载 helperlarge-doc-and-scroll.tsx大文档 shell、promotion、整文档全选/粘贴、滚动行为provider-hooks-contract.tsxprovider/editor 生命周期、useSlateStatic、useSlateSelector、useSlateWithV、editor/readOnly/focused/composing hooks、element hooksreact-editor-contract.tsxwithReact、ReactEditor、DOM 翻译、root/window/shadow-root helper、挂载桥 seamprimitives-contract.tsxZeroWidthString、TextString、SlateText、SlateLeaf、SlateElement、SlateSpacer、SlatePlaceholder、EditableText、EditableTextBlocks、VoidElementeditable-behavior.tsxroot 挂载、DOM 到快照的调和reconciliation、keydown/paste 转发、readOnly、受控替换、rich-inline 锚点重置/重新聚焦projections-and-selection-contract.tsx投影 store 行为、range-ref 支撑的投影、root/node ref hooks、selector 失效局部性app-owned-customization.tsxmarkdown 预览、markdown 快捷键、强制布局forced layout、样式、hovering 工具栏、图片/embed 操作、表格渲染。这种架构的核心思想是“一个文件 一个主导行为域”使未来的回归排查可以做到“一次文件打开”定位。精确的测试所有权迁移清单计划最重的部分是把runtime.tsx中的每一条测试逐条分配到六个抽取文件。以下是完整清单继承自计划原文一条不省。1. provider-hooks-contract.tsxprovider 与 hook 契约迁移这些 runtime 行selector subscriptions stay slice-scoped across a transactionuseSlateStatic returns the provider editor and updates when the provider editor changesSlate initializes fresh editors from initialValue and re-initializes when the provider editor changesSlate publishes onChange, onValueChange, and onSelectionChange on the current snapshot seamslate-react hook surface exposes editor, selection, readOnly, and current boolean contextsslate-react focused and readOnly hooks stay correct outside Editable descendantsslate-react element hooks expose current element context and selected stateuseSlateSelector keeps referential stability when custom equality says values are equaluseSlateWithV exposes the provider editor with the current snapshot versionswitching provider editor instances updates subscribers to the new editor归属理由单一域名provider 与 hook 契约hook 回归排查无需再打开 DOM 桥或渲染原语文件。2. react-editor-contract.tsx挂载桥与 ReactEditor 兼容表面迁移这些 runtime 行withReact and ReactEditor expose the current compatibility seamwithReact composes with withLinks and honors wrapper-owned inline behaviorwithReact composes with withMentions and honors wrapper-owned insertMention behaviorwithReact composes with runtime forced-layout behaviorReactEditor DOM target and event helpers expose the current mounted bridge seamReactEditor root/window helpers expose the mounted document boundaryReactEditor root/window helpers expose the mounted shadow-root boundary归属理由单一域名挂载桥与 ReactEditor 兼容表面与仓库经验mounted bridge 学习精确对齐。3. primitives-contract.tsx渲染与原始组件表面迁移这些 runtime 行slate-react exports the current named render and component prop typesZeroWidthString renders line-break placeholders without FEFF by defaultZeroWidthString retains FEFF for non-linebreak placeholdersTextString repairs stale native text on rerenderSlateText and SlateLeaf own the v2 text-node shapeSlateElement and SlateSpacer own the v2 element and spacer shapeSlatePlaceholder owns the v2 placeholder overlay shapeSlatePlaceholder supports arbitrary intrinsic tags through asEditableText composes text, zero-width, and optional placeholder branchesEditableText forwards arbitrary intrinsic placeholder tagsEditableText supports renderPlaceholderEditableText supports renderTextEditableText splits a text node into projected leaves and refreshes segment dataEditableText exposes text and leafPosition to renderLeafEditableText exposes leaf marks to renderSegmentEditableBlocks exposes renderLeafEditableBlocks forwards renderTextEditableBlocks exposes renderElement attributesEditableBlocks preserves existing text marks during DOM reconciliationEditableText renders zero-length projection slices as mark placeholdersEditableText can derive text and runtime binding from a pathEditableTextBlocks can render from the public editor projectionStore surfaceEditableBlocks aliases the public top-level text-block surfaceEditableBlocks can render mixed inline descendants through the public surfaceEditableBlocks falls back to editor.isInline when no isInline prop is suppliedEditableElement owns the minimal editable element wrapper shapeEditableElement supports arbitrary intrinsic tags through asVoidElement owns the minimal void wrapper and spacer shapeVoidElement supports arbitrary intrinsic tags for wrapper and content归属理由单一域名渲染与原语表面直接遵循 renderer-primitive 学习。这组测试也侧面揭示了 v2 文本渲染管线的分层设计ZeroWidthString处理换行占位默认不用 FEFF、非换行占位保留 FEFF、TextString负责 rerender 时修复过期的原生文本、SlateText/SlateLeaf拥有 v2 文本节点形状、EditableText把一个文本节点拆分为投影叶子projected leaves并刷新 segment 数据且将零长度投影切片渲染为 mark 占位符。4. editable-behavior.tsx挂载的可编辑表面行为迁移这些 runtime 行Editable owns root mounting and DOM-to-snapshot reconciliationEditableBlocks survives text-to-inline replacement on the same path without hook-order crashesEditableBlocks forwards keydown handlers to app-owned keyboard policyEditableBlocks forwards paste handlers to app-owned paste policyEditableBlocks supports readOnly on the structured editing surfaceoptional Activity boundary preserves local state and resumes on latest committed snapshotcontrolled replacement works through package hooks without effect mirroringEditableBlocks rich-inline anchor reset establishes a new history boundary without effect mirroringEditableBlocks rich-inline anchor restores DOM selection on refocus after resetEditableBlocks rich-inline anchor keeps selector invalidation localEditableBlocks keeps unchanged text segments stable across top-level prepends归属理由单一域名挂载的编辑表面行为。这组测试集中体现了 v2 的“无 effect mirroring无副作用镜像”设计立场受控替换与 rich-inline 锚点重置都要求不经过 effect 镜像就能成立且 selector 失效必须保持局部。5. projections-and-selection-contract.tsx投影与 ref 局部性迁移这些 runtime 行root and node ref hooks delegate DOM ownership to slate-domprojection subscriptions stay local when external decoration state changesselection-derived annotation projections track committed selection changesrange-ref-backed projections support persistent annotation anchors归属理由单一域名投影与 ref 局部性语义与通用 provider hooks、与渲染原语都严格分开。注意其中“DOM 所有权委托给 slate-dom”一条——在本仓库中对应packages/slate里的 slate-dom.ts 导出层与packages/slate/src/internal/dom-editor/下的 DOM 辅助函数从源码结构看slate-dom 确实承担toDOMNode、toDOMRange、focus、isFocused等 DOM 边界的实现与测试所述“ref hooks 委托 DOM 所有权给 slate-dom”一致。6. app-owned-customization.tsx应用侧扩展与定制表面迁移这些 runtime 行EditableBlocks supports app-owned markdown preview projectionsEditableBlocks supports app-owned markdown shortcutsEditableBlocks supports app-owned forced layout enforcementEditableBlocks forced layout restores the second paragraph when only a title remainsEditableBlocks supports app-owned styling surfacesEditableBlocks supports app-owned hovering toolbar stateVoidElement supports app-owned editable void controls without mutating editor contentEditableBlocks supports app-owned image and embed void actions归属理由单一域名应用侧扩展与定制表面。这条边界很重要应用app拥有行为策略键盘、粘贴、markdown、样式、工具栏状态而运行时只负责转发与渲染不替应用做决定。7. 保持原样Keep As-Is以下三个文件在拆分中不做迁移surface-contract.tsx、large-doc-and-scroll.tsx、test-utils.ts。执行顺序计划规定了严格的抽取顺序抽取provider-hooks-contract.tsx抽取react-editor-contract.tsx抽取primitives-contract.tsx抽取editable-behavior.tsx抽取projections-and-selection-contract.tsx抽取app-owned-customization.tsx删除runtime.tsx先按域名逐个“掏空”、最后一步才删除源文件保证了每一步都可独立验证、可回滚——这是一次性one-shot拆分下控制风险的基本做法。编辑纪律Editing Rules拆分过程中的六条纪律防止“整理”演变成新的混乱不得复制mountApp或mountAppInShadowRoot一律复用test-utils.ts中的共享 helper每个文件的 import 保持本地于自己的域名若被迁移的测试需要共享工厂数据只抽取窄narrowhelper不要把ReactEditor桥接行混进 provider-hook 文件不要把应用侧定制行混进原语文件若某个文件抽取后仍横跨两个域名就在同一轮内再次拆分。验证Verification拆分全部完成后的验证清单pnpm turbo build --filter./packages/slate-reactpnpm turbo typecheck --filter./packages/slate-reactpnpm --filter slate-react testpnpm lint:fix对每一个新文件做 LSP 诊断检查对以下文件做 architect review架构师评审surface-contract.tsx、provider-hooks-contract.tsx、react-editor-contract.tsx、primitives-contract.tsx、editable-behavior.tsx、projections-and-selection-contract.tsx、app-owned-customization.tsx、large-doc-and-scroll.tsx、test-utils.ts需要说明的前提这些验证命令针对的是计划执行时 Plate 的 slate-v2 工作区含slate-react包的 monorepo 布局依赖pnpmturbo工具链。验收标准Acceptance Criteriaruntime.tsx不再充当无关 React 包行为的默认所有者每个被抽取的文件只有一个主导行为域没有任何 helper 文件被当作测试文件匹配避免test-utils.ts、runtime-fixtures.ts被测试运行器误收集包测试保持绿色且没有隐藏的 fixture 重复未来任何 Agent 打开一个文件就能找到withReact、ReactEditor、projection、primitive、editable behavior或app-owned customization任一回归的归属。最后一条验收标准值得单独品味测试组织的第一消费者是“未来的维护者与 Agent”所有权ownership本身就是文档。风险与缓解计划对三处已知风险给出了明确的缓解策略而不是含糊的“注意”helper 抽取过度演变成“utils 沼泽”utility swamp缓解只共享挂载/设置代码和窄的 fixture 工厂。app-owned 定制行可能仍隐藏跨域子群缓解只有当该文件再次变成垃圾场时才二次拆分。surface-contract.tsx中仍残留部分挂载行为行缓解在这一轮一次性拆分中不动它除非那些行明确属于某个被抽取文件不把计划扩大成“为了折腾而折腾”churn for churns sake。一次打开一个文件这次拆分真正的收获计划的结尾Hard Read只有一句话但点出了全部意图“如果你把这一轮一次性拆分做得好runtime.tsx就不再是‘React 桶’而是要么变得很小、要么彻底死亡。这就是目的。”从方法层面看这份计划对大型前端仓库的测试治理有三点可复用的启示按行为域切分不按行块切分每个测试行都有明确的域名归属理由Why 段落迁移是“语义搬家”而不是“物理切割”这保证了拆分后每个文件可以独立阅读、独立评审。共享基础设施与行为测试严格分层test-utils.ts只放挂载 helperruntime-fixtures.ts只放集中式 fixtures且明确二者不得被测试运行器收集——基础设施的“窄”是行为域所有权清晰的前提。一次性one-shot优于渐进式定义清楚目标架构后按固定顺序七步走完、最后删源文件比在多轮 PR 中反复搬运同一个巨型文件要可靠得多风险用“同轮内再拆分”规则兜底而不是靠事后重构。需要向读者说明的事实边界本计划文档引用的是 Plate 开发过程中 slate-v2 侧仓库的packages/slate-react/test/目录这些文件路径含文档中的绝对路径引用并不存在于当前 Plate 仓库内当前仓库中packages/slate含 slate-dom.ts、internal/dom-editor/等 DOM 边界实现与packages/core中的 React 桥接如 withPlateReact.ts是同一 slate-v2 架构在主仓库中的对应落点。本文所有文件行数为计划文档记录的拆分前基线测试名称均为计划原文逐条继承。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表