Slate 接口体系详解:基于纯 JSON 的可定制富文本编辑器数据模型
2026/9/19 10:53:44 网站建设 项目流程

Slate 接口体系详解:基于纯 JSON 的可定制富文本编辑器数据模型

【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slate

导读

Slate 是当前仓库gh_mirrors/sl/slate中维护的一个"完全可定制"的富文本编辑器框架(当前处于 beta 阶段)。它最核心的设计哲学是:不要求你使用任何框架自带的"模型类",而是直接操作纯 JSON 对象。本文以官方概念文档 docs/concepts/01-interfaces.md 为骨架,结合packages/slate/src/interfaces/下的源码实现,系统讲解 Slate 的接口体系——包括TextElementNodeLocationPath/Point/Range)等核心接口的结构、内置辅助函数,以及如何通过自定义属性和自定义 Helpers 将数据模型塑造成你自己的业务形态。读完本文,你将掌握 Slate 数据模型的最小合法结构、扩展自定义字段的方法,以及利用辅助函数简化文档树操作的全部常用姿势。

一切皆 JSON:接口而非类

大多数富文本编辑器会强迫你使用它们预先定义好的、手写的"模型类"(model class),这给数据引入、序列化、跨端复用都带来了额外的摩擦。Slate 走的是完全相反的路:它只与纯 JSON 对象打交道,唯一的要求是这些对象必须符合 Slate 定义的若干接口(interfaces)

例如,Slate 中的一个文本节点必须符合Text接口:

interface Text { text: string }

这意味着一个文本节点必须有一个text属性,其值为字符串内容:

const textNode = { text: 'Hello, Slate!', }

除了这个最小要求之外,任何其他自定义属性都是允许的,且完全由你决定。这让你可以针对自己的具体领域和用例来定制数据,自由添加任何想要的格式化逻辑,而 Slate 不会横加干涉。

从源码看,Text接口的真实定义位于 packages/slate/src/interfaces/text.ts:

export interface BaseText { text: string } export type Text = ExtendedType<'Text', BaseText>

注意这里的ExtendedType<'Text', BaseText>写法:它来自 packages/slate/src/types/custom-types.ts,作用是"如果用户没有通过 TypeScript 模块声明扩展Text类型,就用默认的BaseText;如果扩展了,就使用用户自定义的类型"。这是 Slate 支持"接口可扩展"这一理念在类型层面的落地,后面"TypeScript 自定义类型"部分会进一步展开。

这种"接口驱动"的方式将 Slate 与大多数要求你操作手写模型类的编辑器区分开来,让数据模型更容易被理解;同时,因为 Slate 从不"初始化"你的数据模型,也避免了启动阶段的性能开销——你的 JSON 数据是什么,Slate 就直接用什么。

核心接口全览:从 Node 到 Location

Text只是 Slate 接口体系的一角。Slate 定义了约十余个接口,全部从入口统一导出,见 packages/slate/src/interfaces/index.ts:

export * from './editor' export * from './element' export * from './location' export * from './node' export * from './operation' export * from './path-ref' export * from './path' export * from './point-ref' export * from './point' export * from './range-ref' export * from './range' export * from './scrubber' export * from './text' export * from './transforms/index'

Node:文档树的联合类型

Node是一个联合类型,代表 Slate 文档树中可能出现的一切节点。见 packages/slate/src/interfaces/node.ts:

export type Node = Editor | Element | Text

同时源码还提供了两个常用窄化类型:

  • Descendant = Element | Text:树中的"后代"节点(不含根Editor);
  • Ancestor = Editor | Element:树中的"祖先"节点(不含叶子Text)。

以及遍历文档树时最常遇到的NodeEntry类型——一个[Node, Path]二元组,表示"某个节点 + 它在文档中的路径":

export type NodeEntry<T extends Node = Node> = [T, Path]

Element:唯一强制属性是 children

Element节点的接口极其宽松,见 packages/slate/src/interfaces/element.ts:

export interface BaseElement { children: Descendant[] } export type Element = ExtendedType<'Element', BaseElement>

它唯一的要求就是必须定义children属性,其中包含该元素的所有子节点(子元素或文本节点)。Element是 Slate 文档树中承载结构的关键:无论是"块级"(block)还是"行内"(inline)元素,都由编辑器配置决定,接口本身并不强制。

Location:Path / Point / Range 三种定位方式

Location是一个联合类型,涵盖三种引用文档位置的方式,见 packages/slate/src/interfaces/location.ts:

export type Location = Path | Point | Range
  • Path(路径)number[],一列索引,精确描述节点在树中的位置。例如[0, 1]表示根节点的第 0 个孩子的第 1 个孩子。见 packages/slate/src/interfaces/path.ts 顶部注释:路径通常相对于根Editor对象,但也可以相对任意Node对象。
  • Point(点){ path: Path; offset: number }path指向文本节点的位置,offset表示深入该节点文本字符串的偏移量。Point 只能指向Text节点。见 packages/slate/src/interfaces/point.ts。
  • Range(范围){ anchor: Point; focus: Point },由两个 Point 组成,表示文档中的一个区间——既可以只覆盖单个节点内部,也可以横跨多个节点。见 packages/slate/src/interfaces/range.ts。

Location接口还提供了isLocationisPathisPointisRangeisSpan等类型守卫。许多 API 会直接接受Location而非强制要求某一种具体类型,这样开发者就不必在自己的代码里手动做类型转换。

接口族谱小结

除上述之外,Slate 还定义了Editor(根节点 + 方法集合)、Operation(描述文档变更的原子操作)、PathRef/PointRef/RangeRef(可随操作自动更新的"引用")、Scrubber(日志脱敏)等接口。完整清单以 packages/slate/src/interfaces/index.ts 的导出为准,后续概念文档(如 docs/concepts/02-nodes.md、docs/concepts/03-locations.md、docs/concepts/05-operations.md)会逐一深入。

自定义属性:让数据模型为你的领域服务

接口的"宽松"正是 Slate 可定制性的根基。以Element为例,你可以为元素(或其他任何接口)扩展与业务领域强相关的自定义属性。比如你可能会有"段落"(paragraph)和"链接"(link)两种元素:

const paragraph = { type: 'paragraph', children: [...], } const link = { type: 'link', url: 'https://example.com', children: [...] }

这里的typeurl就是你自己的自定义 API。Slate 知道它们存在,但不会去使用它们——Slate 只关心children是否存在,以及树的结构是否合法。当渲染链接元素时,你会收到一个携带这些自定义属性的对象,从而可以把它渲染成真实链接:

<a href={element.url}>{element.children}</a>

这种设计让type这类约定俗成的字段既可以在你的代码里自由发挥(区分段落、引用、图片、表格等),又不会污染 Slate 核心的逻辑。测试目录中也能看到这种模式被大量使用,例如 packages/slate/test/interfaces/Element/isElement/ 下的用例就是围绕{ children: [...] }加任意自定义字段的 JSON 结构来验证类型守卫行为的。

辅助函数:每个接口都自带一组工具方法

除了类型信息,Slate 的每一个接口都附带一系列辅助函数(helper functions),让这些纯 JSON 对象用起来毫不费力。这一点在源码中体现为"接口类型 + 同名常量对象"成对出现,例如Node既是类型也是对象(见 packages/slate/src/interfaces/node.ts 中export const Node: NodeInterface = { ... })。

Node 的常用辅助函数

官方文档给出两个典型例子:

import { Node } from 'slate' // 获取元素节点的字符串内容。 const string = Node.string(element) // 获取根节点中某个路径指向的节点。 const descendant = Node.get(value, path)

结合 packages/slate/src/interfaces/node.ts 的源码实现,可以看到它们背后还有一套完整的工具集,按功能可分为几类:

  • 读取与定位Node.get(root, path)(路径取节点,找不到会抛错)、Node.getIf(root, path)(找不到返回undefined)、Node.has(root, path)(判断路径是否存在)、Node.child/Node.parent/Node.ancestor/Node.descendant/Node.leafNode.first/Node.last(取某个分支的第一个/最后一个叶子节点)。
  • 遍历Node.nodes(深度优先遍历整棵树,产出[node, path]条目)、Node.ancestors/Node.descendants/Node.children/Node.levels/Node.texts/Node.elements,这些生成器大多支持reversefrom/topass等选项来控制遍历顺序与跳过条件。
  • 判断与匹配Node.isText/Node.isElement/Node.isEditor/Node.isAncestor/Node.isNode/Node.isNodeList,以及Node.matches(node, props)(按属性子集匹配节点)。
  • 内容提取Node.string(node)拼接节点内容为字符串、Node.texts产出全部文本节点、Node.fragment(root, range)按范围切出一段文档片段。

其中Node.string的实现(同文件第 643-649 行)非常直观:

string(node: Node): string { if (Node.isText(node)) { return node.text } else { return node.children.map(Node.string).join('') } }

即:文本节点直接返回text,元素节点递归拼接所有子节点的字符串。

Range 的常用辅助函数

处理选区(selection)时,Range的辅助函数同样强大:

import { Range } from 'slate' // 按文档顺序获取范围的起点和终点。 const [start, end] = Range.edges(range) // 判断范围是否折叠为单个点(即光标未选中任何内容)。 if (Range.isCollapsed(range)) { // ... }

从 packages/slate/src/interfaces/range.ts 的实现可以看到,Range.edges会根据isBackward的结果把anchor/focus排成正确的先后顺序,Range.isCollapsed则直接比较anchorfocus两个 Point 是否相等。此外还有Range.start/Range.endRange.isBackward/Range.isForward/Range.isExpandedRange.includes(判断是否包含某个 Path/Point/Range)、Range.intersection(求交集)、Range.transform(让选区跟随一次操作自动更新)等。

其他接口的辅助函数也遵循同样的模式:Text提供Text.isTextText.equalsText.matchesText.decorations(按装饰区间把文本切成叶子);Element提供Element.isElementElement.isElementListElement.matchesElement.isElementType(默认检查type键的取值,也可指定其他键);Path提供Path.comparePath.isBefore/Path.isAfterPath.isAncestor/Path.isChildPath.parentPath.transform等;Point提供Point.comparePoint.isBefore/Point.isAfterPoint.transform等。

对于所有常见用例,Slate 都准备了相应的辅助函数。入门阶段通读一遍这些函数非常值得——很多复杂的逻辑往往可以压缩成寥寥几行代码。

自定义辅助函数:把领域逻辑收纳进自己的命名空间

除了内置辅助函数,你几乎总是需要定义自己的辅助函数,并把它们挂到自定义的命名空间上复用。

例如,如果你的编辑器支持图片,你可能会需要一个"判断某元素是否为图片元素"的辅助函数:

const isImageElement = element => { return element.type === 'image' && typeof element.url === 'string' }

这种一次性函数定义起来很简单。但你也可以像核心接口那样,把它们打包进一个命名空间统一使用:

import { Element } from 'slate' // 你可以在任何地方使用 `MyElement` 来获得你的扩展能力。 export const MyElement = { ...Element, isImageElement, isParagraphElement, isQuoteElement, }

这样,领域相关的逻辑就可以与 Slate 内置的辅助函数一起被轻松复用——判断类型、匹配属性、遍历子树等核心能力照旧,图片/段落/引用的专有判断则随取随用。

从类型层面看,这种扩展与 Slate 的ExtendedType机制相辅相成:内置接口的默认类型定义在 packages/slate/src/interfaces/ 各文件中,而 packages/slate/src/types/custom-types.ts 则开放了EditorElementTextSelectionRangePointOperation等键位,允许你通过 TypeScript 的模块声明(declaration merging)为这些接口注入自定义字段,让自定义属性在编译期就获得完整的类型推导。详细的 TypeScript 扩展姿势见官方指南 docs/concepts/12-typescript.md。

实战:把接口体系串起来

把上面的内容串成一个最小可用的例子。假设你要构建一个支持段落与链接的编辑器,你的初始文档(一个Editor节点,本质上是带children和一堆方法的对象)可以这样组织数据:

import { Node, Range } from 'slate' const value = [ { type: 'paragraph', children: [ { text: '访问 ' }, { type: 'link', url: 'https://example.com', children: [{ text: '示例站点' }] }, { text: ' 了解更多。' }, ], }, ] // 读取整篇文档的纯文本内容 const plain = Node.string({ children: value }) // 选中第二段文本的起始位置(path 为 [0, 1] 的文本节点,offset 为 0) const selection = { anchor: { path: [0, 1], offset: 0 }, focus: { path: [0, 1], offset: 4 }, } if (!Range.isCollapsed(selection)) { const [start, end] = Range.edges(selection) // 处理选中区间 ... }

整个过程没有实例化任何编辑器内部类:文档是普通对象,选区是普通对象,读取和判断全靠接口自带的辅助函数。这正是 Slate"接口驱动、JSON 优先"设计理念的直观体现——也是它与传统"模型类"编辑器最根本的区别。

进一步阅读

  • 概念文档:节点结构见 docs/concepts/02-nodes.md,位置与选区见 docs/concepts/03-locations.md,操作模型见 docs/concepts/05-operations.md,TypeScript 类型扩展见 docs/concepts/12-typescript.md。
  • API 参考:每个接口的辅助函数完整签名与说明位于 docs/api/ 下对应的nodes/locations/operations/等目录,例如 docs/api/nodes/node.md、docs/api/locations/range.md。
  • 源码:所有接口的类型与实现集中在 packages/slate/src/interfaces/;对应的行为测试分散在 packages/slate/test/interfaces/ 下,按Node/Element/Path/Point/Range/Text/等子目录组织,是理解每个辅助函数边界行为的最佳参考。

【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slate

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

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

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

立即咨询