ARTICLE DETAIL

资讯详情

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

Slate v2 BaseEditor 状态/事务硬切实践:以 read/update 为唯一公共 API 的架构收口方案

Slate v2 BaseEditor 状态/事务硬切实践:以 read/update 为唯一公共 API 的架构收口方案 Slate v2 BaseEditor 状态/事务硬切实践以 read/update 为唯一公共 API 的架构收口方案【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文基于仓库中的 Slate v2 BaseEditor State/Tx Hard Cut Plan 规划文档完整还原 Slate v2 对BaseEditor公共表面的硬切hard cut过程将editor.read((state) ...)与editor.update((tx) ...)确立为唯一面向普通应用的读写入口删除getSnapshot、getSelection、string、above、pathRef、schema.define等一整套历史遗留的直接实例方法并配套内部运行时拆分、first-party 包迁移、grep 护栏与契约测试。读完本文你将掌握这一事务优先 API 设计的目标形态、state/tx 分组的具体能力清单、九阶段执行路径、以及如何在现有仓库源码中识别切前与切后两种公共表面的差异。一、方案裁决为什么要对公共读取面做硬切规划文档开篇给出的裁决Verdict非常明确对公共BaseEditor实例的查询/读取面执行硬切。理由是当前公共形态同时存在两套相互竞争的 API事务优先的新形态editor.read((state) ...)与editor.update((tx) ...)历史遗留的实例直读/直查形态editor.getSnapshot()、editor.getSelection()、editor.getChildren()、editor.string(...)、editor.above(...)、editor.before(...)、editor.pathRef(...)、editor.schema.define(...)等。文档将其定性为不算一次干净的改写而是仍挂着旧实例 API 的事务优先 API。因此目标公共编辑器被收敛到极小表面export interface EditorV extends Value Value { readT(fn: (state: EditorStateViewV) T): T; update( fn: (tx: EditorUpdateTransactionV) void, options?: EditorUpdateOptions, ): void; subscribe(listener: SnapshotListenerV): () void; extend(extension: EditorExtensionInputV): () void; }其中subscribe被明确定位为高级适配器/运行时桥接advanced adapter/runtime bridge普通应用代码应使用 React hooks 或read/update而不是直接订阅。二、现状证据两套 API 并存的具体表现规划文档的 Current Evidence 部分当时的.tmp/slate-v2工作树列举了三类并存证据。在本仓库的packages/slate源码中可以看到与之对应的切前形态BaseEditor暴露直接读取方法getChildren、getFragment、getLastCommit、getOperations、getSelection、getSnapshot、getRuntimeId、getPathByRuntimeId等BaseEditor暴露直接查询方法above、after、before、edges、first、fragment、hasBlocks、hasInlines、hasPath、isBlock、isEmpty、levels、next、previous、string、unhangRange、void等遍历与文本查询类型层通过OmitFirstArgtypeof Editor.*组装别名把内部静态Editor方法表嵌入公共编辑器类型createEditor()在实例创建时把这些直读/直查别名逐个bindFirst挂载到编辑器对象上。这些现象在本仓库源码中均可直接印证查看 packages/slate/src/create-editor.ts 可以看到大段的Object.assign(editor, { ... bindFirst(...) })别名挂载逻辑如above、string、pathRef、rangeRef、levels、getFragment等而 packages/slate/src/interfaces/editor/editor-api.ts 则是承载这些方法的大型 API 类型定义编辑器核心类型 packages/slate/src/interfaces/editor/editor-type.ts 定义了EditorBase与Editor的关系。此外slate-history、slate-dom、site 示例与测试中大量直接实例读取调用构成了该切面的调用方压力。三、意图与边界清理核心 API而非叠 Plate 语法糖规划文档明确了两点意图让read/update成为唯一正常的公共编辑器状态 API消除文档、选区、操作、schema、遍历、ref、文本的重复读取路径切断公共类型对内部静态Editor实现typeof Editor.*的耦合保持 Slate 本身的无观点unopinionated——这是核心 API 清理不是 Plate 式命令糖。范围之内In scopepackages/slate/src/interfaces/editor.ts、packages/slate/src/create-editor.ts、packages/slate/src/core/public-state.ts、核心包所需的内部运行时助手、slate-dom/slate-history/slate-react/slate-hyperscript调用点、教用户直接读取编辑器的 site 示例与文档、公共表面与类型契约测试。范围之外Out of scope当前版本 Plate 适配器、当前版本 slate-yjs 适配器、React 渲染/运行时事件重写、浏览器选区策略重写、editor.refs作为公共对象的提案以及纯数据命名空间Node、Path、Point、Range、Element、Text、Operation的改动。决策边界Decision boundary是全文最硬核的原则不提供兼容别名、不提供弃用垫片、不写已移除 API文档如果某个 API 只有 core/runtime/tests 需要就把它移到slate/internal或包内私有 helper 之后。四、目标公共 API保留什么、切掉什么保留Keepeditor.read((state) ...) editor.update((tx) ...) editor.subscribe(listener) editor.extend(extension)从公共BaseEditor切掉Cut文档读取getChildren、getFragment、getSelection、getSnapshot、getOperations、getLastCommit运行时 id 读取getRuntimeId、getPathByRuntimeId脏路径读取getDirtyPaths、getOperationDirtiness直接 schema 对象schema作为公共应用方法的直接归一化钩子normalizeNode、shouldNormalize全部直接查询别名above、after、before、edges、first、fragment、hasBlocks、hasInlines、hasPath、hasTexts、isBlock、isEdge、isEmpty、isEnd、isNormalizing、isStart、last、leaf、levels、next、parent、path、point、positions、previous、projectRange、range、string、unhangRange、void、shouldMergeNodesRemovePrevNoderef 别名pathRef、pathRefs、pointRef、pointRefs、rangeRef、rangeRefs公共BaseEditor中任何OmitFirstArgtypeof Editor.*引用。五、State View所有存续读取的唯一归宿规划要求把EditorStateView扩展到每个存续的读取都有一个明确的归属从而做到没有任何直接编辑器实例查询是正常读取路径。完整能力清单如下editor.read((state) { state.value.get(); state.value.snapshot(); state.value.operations({ since: 0 }); state.value.lastCommit(); state.selection.get(); state.marks.get(); state.nodes.children([]); state.nodes.get([0]); state.nodes.parent([0, 0]); state.nodes.above({ at, match }); state.nodes.first(at); state.nodes.last(at); state.nodes.leaf(at); state.nodes.levels({ at }); state.nodes.next({ at }); state.nodes.previous({ at }); state.nodes.match({ at }); state.nodes.hasPath(path); state.nodes.isBlock(element); state.nodes.isEmpty(element); state.nodes.hasBlocks(element); state.nodes.hasInlines(element); state.nodes.hasTexts(element); state.nodes.void({ at }); state.points.before(at, options); state.points.after(at, options); state.points.start(at); state.points.end(at); state.points.get(at, options); state.points.isEdge(point, at); state.points.isStart(point, at); state.points.isEnd(point, at); state.ranges.get(at); state.ranges.edges(at); state.ranges.unhang(range, options); state.ranges.project(range); state.text.string(at, options); state.schema.getElementSpec(type); state.schema.isInline(element); state.schema.isBlock(element); state.schema.isVoid(element); state.schema.isElementReadOnly(element); state.schema.isSelectable(element); state.schema.markableVoid(element); state.runtime.idAt(path); state.runtime.pathOf(runtimeId); });从分组结构看state.value文档值与操作历史、state.selection/state.marks选区与标记、state.nodes节点遍历、state.points点几何、state.ranges范围几何、state.text文本提取、state.schema元素规格、state.runtime运行时 id 映射各司其职。其中state.nodes.*的语义可在本仓库packages/slate/src/internal/editor/目录下的实现文件中找到对应物如 above.ts、getLevels.ts、getVoidNode.ts、getEditorString.ts 等——这些正是被切掉实例别名后、由 state/tx 分组转调的内部实现。六、Transaction View事务内继承只读分组并追加写入tx继承state的读分组并追加写入能力editor.update((tx) { tx.value.replace(value); tx.operations.replay(ops); tx.nodes.set(props, options); tx.nodes.insert(node, options); tx.nodes.remove(options); tx.nodes.move(options); tx.nodes.wrap(element, options); tx.nodes.unwrap(options); tx.nodes.split(options); tx.nodes.merge(options); tx.selection.set(target); tx.selection.clear(); tx.selection.move(options); tx.selection.collapse(options); tx.text.insert(text, options); tx.text.delete(options); tx.marks.add(key, value); tx.marks.remove(key); tx.marks.toggle(key, options); tx.normalize(options); tx.withoutNormalizing(fn); });关于 refs 的归属规划给出了两个选项若 refs 仍保留在公共面则放进 state/tx 分组而非挂在editor上editor.update((tx) { const ref tx.refs.path(path, options); });若 refs 只是运行时基础设施则整体迁往slate/internal。Phase 2 会在删除别名前先做此裁决。本仓库中createPathRef、createPointRef、createRangeRef等 ref 工厂位于 packages/slate/src/internal/editor/这与refs 偏向运行时基础设施的实现方向一致。七、扩展与 Schema用 extend 取代 editor.schema.define公共 schema 变更不应再走editor.schema.define(...)目标形态是显式扩展注册const cleanup editor.extend( defineEditorExtension({ elements: [ { type: image, void: block, selectable: true, }, ], }), );schema 事实一律通过state.schema/tx.schema读取。内部包可以继续使用运行时 schema 注册表但公共编辑器对象不再暴露可变 schema 对象。扩展的覆盖点override points由此变为显式的扩展/运行时注册点而不是在公共编辑器对象上猴子补丁方法。八、内部运行时目标实例对象之外存放被切方法规划引入一个内部运行时对象统一托管被移除的直接方法type EditorRuntimeV extends Value Value { state: InternalEditorStateApiV; queries: InternalEditorQueryApiV; transforms: EditorTransformRegistryV; refs: InternalEditorRefApi; schema: InternalEditorSchemaApi; normalize: InternalEditorNormalizeApi; };配套规则createEditor()返回小而公共的编辑器对象内部运行时以编辑器身份editor identity为键、带外out-of-band存储核心实现文件调用内部运行时 helper而不是公共实例别名slate/internal仅向 first-party 包导出内部 helper公共类型不再出现EditorStaticApi、InternalEditor或OmitFirstArgtypeof Editor.*。这保证了扩展覆盖点依然存在只是以显式扩展/运行时注册的形态存在。九、迁移骨架Plate 与 slate-yjs 如何对接Plate 迁移骨架Plate 通过editor.extend(...)安装行为Plate 通过扩展注册新增 state/tx 分组Plate 命令调用editor.update((tx) ...)Plate UI hooks 通过节点/运行时 id 选择器订阅而非宽泛的编辑器实例读取。slate-yjs 迁移骨架协作读取快照/提交通过state.value.snapshot()与state.value.lastCommit()远程操作通过tx.operations.replay(...)应用运行时 id 通过state.runtime保持可用editor.subscribe(...)作为高级适配器桥保留。规划明确不要求与当前版本适配器保持兼容。十、九阶段执行路径Phase 1红色公共表面契约先写测试断言BaseEditor公共键恰好为read/update/subscribe/extend断言editor.getSnapshot、editor.getSelection、editor.getChildren、editor.string、editor.above、editor.pathRef、editor.schema是类型错误且运行时不存在断言公共BaseEditor源码无OmitFirstArgtypeof Editor断言公共根slate仍导出type Editor类型而非运行时断言纯数据命名空间Node/Path/Point/Range/Element/Text/Operation仍可用。聚焦命令bun test ./packages/slate/test/public-surface-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts预期实现前为红色。Phase 2补全 State/Tx 读取覆盖在删除别名前补齐缺失的 state 分组state.value.snapshot()、state.value.operations(options?)、state.value.lastCommit()、state.runtime.idAt(path)、state.runtime.pathOf(runtimeId)、state.nodes下的above/first/last/leaf/levels/next/previous/void/hasBlocks/hasInlines/hasTexts/isBlock/isEmpty、state.points下的get/isEdge/isStart/isEnd、state.ranges下的unhang/project、state.schema.isBlock。事务等价物只在需要变更或事务本地读取语义时才添加。Phase 3内部运行时拆分把直接查询/读取实现迁到内部 helper 之后新增getEditorRuntime(editor)、getEditorQueryRuntime(editor)、getEditorSchemaRuntime(editor)、getEditorRefRuntime(editor)、getEditorNormalizeRuntime(editor)并改造public-state.ts让state/tx直接调用内部 helper而不再回叫editor.above(...)、editor.string(...)等实例别名。Phase 4剪切 BaseEditor编辑packages/slate/src/interfaces/editor.ts收缩BaseEditor、移除公共直读/直查/schema/归一化钩子、移除或迁移 ref 别名、移除OmitFirstArgtypeof Editor.*。编辑packages/slate/src/create-editor.ts停止向公共对象挂载被删方法、改为初始化内部运行时注册表、仅保留read/update/subscribe/extend。Phase 5First-party 包迁移slate-historyEditor.getSnapshot(e)→e.read((state) state.value.snapshot())Editor.subscribe(e, ...)可保留为e.subscribe(...)高级适配器桥历史写入保持在e.update((tx) ...)内slate-dom模型读取使用editor.read(...)仅 DOM 的 helper 归 DOM 所有内部 map 种子数据使用slate/internalslate-react测试与运行时使用 state/tx 或运行时门面热路径保持节点/运行时 id 选择器slate-hyperscriptfixture 搭建使用 state/tx 或包内私有 helper。Phase 6测试与 Fixture公共 API 测试一律走editor.read/editor.update内部实现测试显式从slate/internal导入删除只为证明被删别名存在的测试遍历、refs、schema、归一化、选区的行为契约全部保留但改由 state/tx/internal 归主。不做 preload 重写不建兼容桥。Phase 7文档与示例文档只描述当前事实正常读取editor.read((state) ...)、正常写入editor.update((tx) ...)、扩展/schema 安装editor.extend(...)不再出现editor.getSnapshot()、editor.getChildren()、editor.string(...)、editor.schema.define(...)与任何Editor.*状态/查询 helper。Phase 8grep 护栏rg -n OmitFirstArgtypeof Editor packages/slate/src/interfaces/editor.ts rg -n \beditor\.(getChildren|getSelection|getSnapshot|getOperations|getLastCommit|getRuntimeId|getPathByRuntimeId|string|above|after|before|pathRef|pointRef|rangeRef|schema)\b packages site docs --glob !**/dist/** --glob !site/out/** --glob !site/.next/** rg -n import \{[^}]*Editor[^}]*\} from [\]slate[\] packages site docs --glob !**/dist/**允许的命中只能是显式内部测试或历史 changelog。Phase 9验证标记完成前必须通过bun test ./packages/slate/test/public-surface-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts bun test ./packages/slate/test/query-contract.ts ./packages/slate/test/snapshot-contract.ts bun test ./packages/slate-history/test/history-contract.ts ./packages/slate-history/test/integrity-contract.ts bun check若文档/示例改变了用户可见的浏览器行为需补充聚焦的slate-browser或 Playwright 证明bun test:integration-local不进入常规迭代循环。十一、风险登记表风险为什么重要应对state/tx 分组漏掉某个现有查询移除会变成 DX 回归Phase 2 在剪切前补齐 parity内部包再次导入公共别名硬切会腐烂Phase 8 grep 护栏扩展作者失去覆盖点插件需要行为钩子覆盖点迁移到显式扩展/运行时注册协作丢失快照/提交访问slate-yjs 类基座需要确定性提交state.value.snapshot()、state.value.lastCommit()、tx.operations.replay(...)、editor.subscribe(...)Ref API 变得模糊refs 既非纯读取也非普通写入Phase 2 删除别名前在 state/tx/internal 间裁决文档残留过期片段用户学到错误 APIPhase 7 配合 grep 护栏十二、完成标准Done CriteriaBaseEditor公共表面小而干净不暴露直接状态/查询别名createEditor()返回仅含read/update/subscribe/extend四个公共方法的对象state与tx覆盖所有预期的公共读写公共类型不再依赖typeof Editor.*first-party 包使用 state/tx 或显式内部 helper文档/示例只教一条公共生命周期无兼容桥、别名、弃用垫片或回退残留聚焦契约测试通过.tmp/slate-v2中bun check通过。十三、执行日志从红色契约到最终完成执行日志完整记录了该计划的落地轨迹也展示了硬切过程中对诚实完成的坚持Ralph Activation将目标状态置为 pending开始 Phase 1 红色契约Phase 1 红色契约新增公共表面契约与 state/tx 契约运行bun test .../public-surface-contract.ts .../state-tx-public-api-contract.ts结果为预期的163 pass, 5 fail公共表面、文档与护栏清扫BaseEditor收敛为四方法、createEditor()不再挂载直读/直查/schema/ref 别名、内部运行时迁移到 runtime helpers 与slate/internal、移除OmitFirstArg死代码docs/examples 完成editor.schema.define(...)→editor.extend(...)等迁移slate-react 的bun test:vitest test/surface-contract.test.tsx通过bun check全绿Phase 6 fixture 桥剪切移除with-test.js中的遗留桥接方法getChildren、getSelection、getOperations、schema.define、直接 transform 别名将旧 fixture 迁移到tx.value.operations()、tx.operations.replay(...)、tx.marks.add(...)、tx.nodes.*、tx.withoutNormalizing(...)、editor.extend(...)验证结果964 pass, 94 skipslate、993 pass, 94 skip含 slate-hyperscript、契约测试168 pass、bun typecheck:packages6 个包全部成功。期间发现未被默认 Bun 发现的陈旧契约文件仍编码旧别名完成状态一度从 done 退回 pending陈旧契约清理删除surface-contract.ts与extension-contract.ts等仅为旧 API 存在的文件迁移存续契约到editor.read(...)/editor.update((tx) ...)/editor.extend(...)/ 测试专用support/schema.ts最终契约计数591 pass全量993 pass, 94 skipbun lint:fix、bun typecheck:packages、bun check全部通过完成状态置为done。这一段日志最具参考价值的实践是硬切不只是删代码还包括删除只为证明旧 API 存在而写的测试以及用明确的验收命令契约测试 bun check grep 零命中把完成变成可验证的事实。十四、本仓库对照如何观察切前与切后需要说明的是该规划文档指向的.tmp/slate-v2是规划执行期的独立工作树本仓库当前检入的packages/slate呈现的是规划文档Current Evidence一节所描述的切前形态两者恰好构成一组对照切前本仓库packages/slatecreate-editor.ts 中大量Object.assign(editor, { ... bindFirst(...) })别名挂载、editor-api.ts 的大型 API 类型、editor-type.ts 中的EditorBase定义切后规划文档描述的目标态公共对象只有read/update/subscribe/extend查询与写入全部收敛到 state/tx 分组内部实现沉淀在slate/internal。这种对照关系对读者极有价值当你看到packages/slate/src/internal/下成体系的 above.ts、getEditorString.ts、createPathRef.ts、insertNodes.ts 等实现文件时就能理解它们正是硬切目标中内部运行时的天然落点——公共面被清空后这些实现只需换一个访问入口即可被 state/tx 分组复用。结语这份 BaseEditor State/Tx 硬切方案是 Slate v2 走向事务优先架构的关键一步它用一个小而严格的公共接口read/update/subscribe/extend取代了历史遗留的直接实例 API 集群通过 state/tx 分组让每个读取与写入都有唯一归属通过内部运行时与slate/internal保住扩展点与 first-party 包的能力再用红色契约测试、grep 护栏与明确的验收命令把重构约束在不腐烂的轨道上。对于正在设计编辑器内核 API、或准备对自己的库做破坏性接口收敛的团队这份文档从目标形态、迁移骨架到执行日志都提供了可直接借鉴的完整方法论。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表