Slate v2 `core.ts` 彻底解剖实战:一方法一文件的引擎级代码拓扑重构方案
2026/9/16 11:26:14 网站建设 项目流程

Slate v2core.ts彻底解剖实战:一方法一文件的引擎级代码拓扑重构方案

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

本文基于 docs/plans/2026-04-10-slate-v2-core-ts-complete-dissection-plan.md 展开。该文档是 plate 仓库中 Slate v2 引擎对core.ts单体文件进行的“彻底解剖”计划:把所有值得独立成路径的公共/核心行为拆到src/core/*(本仓库中落地为src/internal/*src/interfaces/*),让core.ts不再隐藏主要导出运行时行为、删除死的兼容路径,并以“一方法一文件”的拓扑维护引擎公共面。读完本文,你将掌握这套代码解剖的目标、治理规则、决策矩阵、四阶段执行顺序、逐片验证流程与完成判定标准,并能对照当前仓库packages/slate/src的实际结构确认拆分的落地结果。

背景:为什么必须对core.ts做“彻底”解剖

Slate 早期将引擎核心运行时逻辑集中在一个core.ts大文件中:事务入口、ref 重定位、脏路径处理、规范化、快照发布、fragment 容器插入栈等行为全部交织在同一主体内。这种“omnibus 文件”带来的问题在引擎重写方向上会被持续放大:

  • 公共/核心行为没有独立归属路径,改动一处牵动全局;
  • 文件树与真实所有权不一致,core.ts成为“隐藏的真正所有者”;
  • 死掉的兼容路径以“假所有权”的形式被保留,维护者难以判断哪些逻辑还活着;
  • 跨路径共享的基础代码变成偶然的“万能文件”,而不是被显式声明的内部支撑模块。

该计划的Goal非常明确:完成对core.ts的解剖,而不是半途而废。其终态(end state)有三条:

  1. 每个值得拥有独立路径的公共/核心行为,都落在src/core/下的独立文件里;
  2. core.ts不再隐藏任何主要的导出运行时行为;
  3. 死的兼容路径被直接删除,而不是作为“假所有权”保留;跨多个路径文件共享的 helper/基础设施代码可以继续共享,但必须被显式当作内部支撑(internal support),而不是偶然的 omnibus 所有者。

治理规则:只保留能强化“更好引擎方向”的漂移

解剖不是机械的文件搬家。文档给出了一条Governing Rule(治理规则):只保留能强化更好引擎方向的漂移,其余一律回退。所谓“更好引擎方向”共五条:

  1. data-model-first(数据模型优先);
  2. operation- and collaboration-friendly(面向操作与协作友好);
  3. transaction-first engine semantics(事务优先的引擎语义);
  4. React-optimized runtime(面向 React 优化的运行时);
  5. explicit adapters later(显式适配器后置)。

对于packages/slate/src/**的具体适用规则是:

  • 公共/核心表面代码保持one-file-per-method 拓扑(一方法一文件);
  • 如果某项漂移不能明确支持上述更好引擎方向,就回退它;
  • 当某个路径被设定为“真实所有者”时,绝不允许留下假包装(fake wrapper)伪装所有权;
  • 绝不为了“让文件树看起来眼熟”而发明死的兼容文件。

当前状态盘点:已经抽出与已经剪除的路径

文档将解剖进度分为三类,逐文件登记。这是理解整个拆分工作的“存量账本”,也是后续任何切片(slice)都必须对照更新的基准。

已抽入真实src/core/*所有者的文件

公共/核心运行时表面中,以下行为已拥有独立所有权文件:

行为说明
apply.tsapply(editor, op)入口,其余逻辑委托给withTransaction(...)getState(...)applyOperation(...)
get-dirty-paths.ts脏路径收集,本地 path helpers + 直接后代收集替代旧Path.levels/Path.ancestors/Node.nodes命名空间调用
get-fragment.ts直接从当前快照/树形结构切片 fragment,不再委托Node.fragment(editor, selection)
normalize-node.ts收窄为当前模型:explicit通道、直接子节点校验、inline 兼容后代收集、局部fallbackElement处理
should-normalize.ts删除旧的initialDirtyPathsLength最大迭代守卫,仅保留当前默认return true
get-current-node.ts/get-current-selection.ts/get-current-marks.ts/get-current-children.ts/get-current-range-for-path.ts/get-current-index.ts/get-current-replace-epoch.ts当前运行时读取面,此前全部埋在core.ts
get-snapshot.ts/replace-snapshot.ts快照读取与替换(replaceSnapshot(editor, input)
subscribe.tssubscribe(editor, listener)订阅面
set-current-marks.tssetCurrentMarks(editor, marks)
get-range-refs.ts/range-ref.tsgetRangeRefs(editor)rangeRef(editor, range, affinity)
initialize-editor.tsinitializeEditor(editor)初始化面

已抽入显式内部支撑模块的文件

这些文件仍被多个路径文件共享,但它们本身不是用户视角下的“行为名”,因此作为显式 internal support 存在:

  • path-helpers.ts:路径/点比较、range 边界解析、path-key 生成;
  • node-helpers.ts:节点克隆、属性拷贝、后代切片、path→node 遍历;
  • draft-helpers.ts:draft 克隆、draft-index 创建、快照物化、draft 子节点遍历;
  • transaction-helpers.ts:规范化入口收集、自定义 normalizer 循环、已创建 range-ref 发布、快照发布;
  • fragment-rebase-helpers.ts:折叠插入 rebase、range 替换 rebase、move-target 调整、scratch-editorinsert_fragment模拟;
  • fragment-helpers.ts:remove-node 选择回退、混合 inline/block 容器分析、fragment slice/split、insertFragment*执行栈。

已作为死路径剪除的文件

  • batch-dirty-paths.ts
  • update-dirty-paths.ts

这两条路径只在当前引擎重新使用它们时才允许恢复,否则视为死的兼容路径。

仍直接留在core.ts中的内容

按文档记录,core.ts中仅剩共享类型声明与INTERNAL状态,不再有任何主要导出运行时行为主体。

决策矩阵:哪些该共享、哪些该成为真实所有者

文档的Decision Matrix把待处理项分成两类,避免“为了美学把每个微小 helper 都拆成文件”的反模式。

A 类:应成为显式内部共享模块,而不是留在core.ts

这些逻辑仍被多个已抽出路径文件需要,但它们不是评审者视角下的行为名:

  • 混合 inline 与 block 容器的 fragment 支持;
  • fragment slice/split helpers;
  • insert-fragment rebasing helpers;
  • scratch-editor fragment 模拟。

推荐去向:packages/slate/src/core/下一个显式内部支撑文件,工作名即fragment-helpers.ts。规则明确警告:如果只是把每个微小 helper 拆成独立文件而没有任何语义增益,就不要拆

B 类:剩余的、真实的导出核心所有者

该 tranche 已全部完成,共四个文件:

  • get-state.ts:内部状态查找,抽出后的核心文件直接调用;
  • step-current-point.tsbefore(...)/after(...)跨受支持顶层文本块的 point 步进;
  • apply-operation.ts:事务 draft 上的原始 operation 应用,仍是apply(...)下的独立步骤;
  • with-transaction.ts:事务进入/退出、显式规范化设置、发布时序。

执行顺序:四个阶段的解耦策略

解剖被拆成四个阶段,每个阶段解决一类问题:

Phase 1:冻结当前 tranche

  • 保持已抽出文件全绿(测试通过);
  • 除非当前引擎真的再次使用,否则不重开任何死路径;
  • 任何路径状态变化时,立即同步更新pr-description.md(本仓库对应文件为 docs/slate-v2-draft/references/pr-description.md),它是面向维护者的 drift 登记册。

Phase 2:把导出的运行时所有者移出core.ts

按此顺序执行:get-state.tsstep-current-point.tsapply-operation.tswith-transaction.ts

理由:这四者正是让core.ts仍像“真正所有者”的导出运行时件;优先移动它们,能在不强迫先把每个微小 helper 单独成文件的前提下,获得最大的“真相收益”。

Phase 3:从core.ts中雕出内部共享 helpers

状态(文档记录时):path helpers、node helpers、draft/snapshot helpers、transaction publish helpers、fragment rebasing/simulation helpers、混合 inline/block fragment helpers、fragment slice/split helpers、fragment insertion helpers 均已完成。

规则:

  • 按连贯的职责分组;
  • 避免“每个琐碎 helper 一个文件”的无意义碎片化;
  • 保持 import 简单、明显。

Phase 4:压平core.ts

完成态判定:

  • core.ts变为显式导出表面,不再是引擎的真正居所;
  • 不存在兜底的export *
  • 每个 re-export 都指向真实的 owner/support 文件。

明确禁止的事项(What not to do)

文档用负面清单锁死边界:

  • 不要恢复batch-dirty-paths.tsupdate-dirty-paths.ts这类死路径,除非当前引擎真的再次使用它们;
  • 不要为了达成审美理想而逐个抽取微小 helper;
  • 不要为公共/核心行为名留下假包装(fake wrappers);
  • 不要在路径文件存在后,仍把core.ts当作秘密的真正所有者。

每个提取切片的验证规则

文档强调“切片必须有验证”,每个 slice 都要执行五步:

  1. 读取目标文件的精确 git diff;
  2. 确认新 owner 文件包含真实逻辑,而不是 shim 桩;
  3. 对受影响文件及其直接调用者重跑一次窄范围 TypeScript 探针:tsc --noEmit --skipLibCheck --target es2022 --module esnext --moduleResolution bundler ...
  4. pr-description.md中记录具体的 old-vs-current 文件漂移;
  5. 如果某个路径经核查并非当前真实概念,就剪掉它,而不是保留成“兼容剧场”(compatibility theater)。

完成判定标准

解剖只有在以下条件全部满足时才宣告完成:

  • core.ts不再包含任何主要导出运行时行为主体;
  • 每个存活的导出src/core/*路径都是真实所有者;
  • 死的兼容路径全部消失;
  • pr-description.md逐文件具体解释了存活的漂移;
  • 维护者文档中不再有任何一行仍在使用假的 “owns logic” 填充语。

当前仓库的落地印证:从计划到packages/slate/src

解剖计划在本仓库已经落地为实际的目录结构。对照 packages/slate/src/index.ts 可以看到,包的导出表面由create-editorslate-domtypesinterfacesslate-historyutils组成,不存在一个居中承担全部运行时行为的core.ts

具体印证点包括:

  • 一方法一文件拓扑:运行时方法各自拥有独立文件。例如编辑器查询面在 packages/slate/src/internal/editor/ 下有getMarks.tsgetFragment.tsnormalizeNode.tsnormalizeEditor.tsabove.tsgetPointBefore.ts等;转换面在 packages/slate/src/internal/transforms/ 下有deleteText.tsinsertFragment.tsinsertNodes.tssetSelection.tsmergeNodes.ts等;扩展转换面在 packages/slate/src/internal/transforms-extension/ 下有toggleBlock.tstoggleMark.tsduplicateNodes.ts等。
  • 显式导出表面而非export *create-editor.ts通过bindFirst把每个独立模块的方法装配到 editor 实例(见 packages/slate/src/create-editor.ts),例如getMarks: bindFirst(getMarks, editor)insertFragment: bindFirst(insertFragment, editor)setSelection: bindFirst(setSelection, editor),与计划“每个 re-export 指向真实 owner 文件”的 Phase 4 终态一致。从internal/transforms/setSelection.ts的源码可以看到薄包装形态:setSelection内部委托给从slate导入的setSelection as setSelectionBase,路径文件负责所有权、实现留在内核。
  • 接口命名空间拆分:类型与守卫由 packages/slate/src/interfaces/ 下的path.tspoint.tsrange.tslocation.tsnode.tstext.tselement.tsoperation.ts等分别持有;编辑器 API 形状集中在 packages/slate/src/interfaces/editor/editor-api.ts,其中显式列出fragmentgetDirtyPathsgetFragmentsetNormalizingshouldNormalize等当前运行时表面方法。
  • 测试佐证:packages/slate/src/create-editor.spec.ts 验证了“方法所有权与实例装配”这一解剖的直接结果——例如expect(editor.getMarks).toBe(editor.api.marks)expect(editor.insertText).toBe(editor.tf.insertText),以及withHistory包装后editor.undo/redo/applyeditor.tf的对应关系。
  • 漂移登记册:文档要求同步维护的pr-description.md在本仓库对应为 docs/slate-v2-draft/references/pr-description.md,其中逐文件登记了packages/slate的 121 个变更根/源文件与 1048 个删除测试文件的归属理由,并明确写着core.ts“被缩减为共享核心类型、INTERNAL状态与显式 re-export,文件树终于与运行时逻辑的真实所在位置一致”,正是本计划 Phase 4 终态的注脚。

对后续维护者的实践启示

这套解剖方案的价值不在于“把大文件拆小”本身,而在于它建立了一套可重复的判定闭环:治理规则决定是否允许漂移 → 决策矩阵决定文件归属(真实 owner 还是内部 support)→ 四阶段顺序控制解耦节奏 → 五步验证规则保证每个切片都有 diff、类型探针与登记册背书 → 完成标准防止半途而废。对本仓库的 Slate v2 引擎而言,它意味着任何新加的公共/核心方法都应直接落在自己的路径文件中,共享基础设施必须显式声明为 internal support,而任何无法证明自己还活着的兼容路径都应被剪除而非收藏。

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

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

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

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

立即咨询