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)有三条:
- 每个值得拥有独立路径的公共/核心行为,都落在
src/core/下的独立文件里; core.ts不再隐藏任何主要的导出运行时行为;- 死的兼容路径被直接删除,而不是作为“假所有权”保留;跨多个路径文件共享的 helper/基础设施代码可以继续共享,但必须被显式当作内部支撑(internal support),而不是偶然的 omnibus 所有者。
治理规则:只保留能强化“更好引擎方向”的漂移
解剖不是机械的文件搬家。文档给出了一条Governing Rule(治理规则):只保留能强化更好引擎方向的漂移,其余一律回退。所谓“更好引擎方向”共五条:
- data-model-first(数据模型优先);
- operation- and collaboration-friendly(面向操作与协作友好);
- transaction-first engine semantics(事务优先的引擎语义);
- React-optimized runtime(面向 React 优化的运行时);
- explicit adapters later(显式适配器后置)。
对于packages/slate/src/**的具体适用规则是:
- 公共/核心表面代码保持one-file-per-method 拓扑(一方法一文件);
- 如果某项漂移不能明确支持上述更好引擎方向,就回退它;
- 当某个路径被设定为“真实所有者”时,绝不允许留下假包装(fake wrapper)伪装所有权;
- 绝不为了“让文件树看起来眼熟”而发明死的兼容文件。
当前状态盘点:已经抽出与已经剪除的路径
文档将解剖进度分为三类,逐文件登记。这是理解整个拆分工作的“存量账本”,也是后续任何切片(slice)都必须对照更新的基准。
已抽入真实src/core/*所有者的文件
公共/核心运行时表面中,以下行为已拥有独立所有权文件:
| 行为 | 说明 |
|---|---|
apply.ts | apply(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.ts | subscribe(editor, listener)订阅面 |
set-current-marks.ts | setCurrentMarks(editor, marks) |
get-range-refs.ts/range-ref.ts | getRangeRefs(editor)与rangeRef(editor, range, affinity) |
initialize-editor.ts | initializeEditor(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.tsupdate-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.ts:before(...)/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.ts→step-current-point.ts→apply-operation.ts→with-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.ts、update-dirty-paths.ts这类死路径,除非当前引擎真的再次使用它们; - 不要为了达成审美理想而逐个抽取微小 helper;
- 不要为公共/核心行为名留下假包装(fake wrappers);
- 不要在路径文件存在后,仍把
core.ts当作秘密的真正所有者。
每个提取切片的验证规则
文档强调“切片必须有验证”,每个 slice 都要执行五步:
- 读取目标文件的精确 git diff;
- 确认新 owner 文件包含真实逻辑,而不是 shim 桩;
- 对受影响文件及其直接调用者重跑一次窄范围 TypeScript 探针:
tsc --noEmit --skipLibCheck --target es2022 --module esnext --moduleResolution bundler ... - 在
pr-description.md中记录具体的 old-vs-current 文件漂移; - 如果某个路径经核查并非当前真实概念,就剪掉它,而不是保留成“兼容剧场”(compatibility theater)。
完成判定标准
解剖只有在以下条件全部满足时才宣告完成:
core.ts不再包含任何主要导出运行时行为主体;- 每个存活的导出
src/core/*路径都是真实所有者; - 死的兼容路径全部消失;
pr-description.md逐文件具体解释了存活的漂移;- 维护者文档中不再有任何一行仍在使用假的 “owns logic” 填充语。
当前仓库的落地印证:从计划到packages/slate/src
解剖计划在本仓库已经落地为实际的目录结构。对照 packages/slate/src/index.ts 可以看到,包的导出表面由create-editor、slate-dom、types、interfaces、slate-history、utils组成,不存在一个居中承担全部运行时行为的core.ts。
具体印证点包括:
- 一方法一文件拓扑:运行时方法各自拥有独立文件。例如编辑器查询面在 packages/slate/src/internal/editor/ 下有
getMarks.ts、getFragment.ts、normalizeNode.ts、normalizeEditor.ts、above.ts、getPointBefore.ts等;转换面在 packages/slate/src/internal/transforms/ 下有deleteText.ts、insertFragment.ts、insertNodes.ts、setSelection.ts、mergeNodes.ts等;扩展转换面在 packages/slate/src/internal/transforms-extension/ 下有toggleBlock.ts、toggleMark.ts、duplicateNodes.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.ts、point.ts、range.ts、location.ts、node.ts、text.ts、element.ts、operation.ts等分别持有;编辑器 API 形状集中在 packages/slate/src/interfaces/editor/editor-api.ts,其中显式列出fragment、getDirtyPaths、getFragment、setNormalizing、shouldNormalize等当前运行时表面方法。 - 测试佐证: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/apply与editor.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),仅供参考