富文本总结(二):Slate概念
架构
Slate 并非一个做到开箱即用的富文本应用,而是提供了基本富文本能力的框架。而其他功能都需要开发者自行通过插件的形式进行实现。
特性
- 
Immutable:基于Immutable,利用
Immer库,每次只会生成修改的数据,其他相同的部分和原有数据共享。 - 
非常轻量:不同于其他编辑器类的库,Slate并不提供譬如粗体、斜体、字体色等开箱即用的功能。Slate只是提供了一套自己定义的核心数据模型,以此一些操作数据和选区相关的API
 - 
视图无关:视图层的渲染和行为完全由开发者基于React定制。
 - 
协同编辑:从顶层设计上看,Slate的架构是典型的MVC模型,由自身定义数据模型(Model),暴露操作数据的方法(Controller),然后交由用户使用该数据在React中做渲染(View)。Slate.js 的模型设计天然就亲和协同编辑。
 
源码
slate项目使用monorepo的架构,共有三个包:
- 
slate
slate内核,定义编辑器的数据结构(Model),提供操作数据的API(Controller)。
 - 
slate React
基于React实现的slate视图层(View)。
 - 
slate History
一个slate的插件,实现slate的undo、redo功能
 
数据结构
Slate.js 的数据结构设计大量参考了 HTML 中对于 DOM 的设计,
Web 富文本,其实就是一段 HTML 内容,它由两个部分组成:
- 节点(Node):节点容纳了我们能看到的富文本内容,富文本容器也是一个节点,容纳了其他节点。
 - 选区(Selection):当前选中的区域,如果区域的起点和终点重合,那就是一个光标。
 
Editor
Editor存储了Slate编辑器的所有状态,并可以通过插件来进行扩展。
interface Editor {
  children: Node[] //节点的结构 
  selection: Range | null/ // 选区
  operations: Operation[] //即将执行的操作
  marks: Omit<Text, 'text'> | null // text的标记
  // Schema-specific node behaviors. 节点行为
  isInline: (element: Element) => boolean // 是否为内联节点。
  isVoid: (element: Element) => boolean // 是否为空节点。
  normalizeNode: (entry: NodeEntry) => void //进行格式化。
  onChange: () => void //修改事件
  // Overrideable core actions.
  addMark: (key: string, value: any) => void  //增加标记
  apply: (operation: Operation) => void // 应用操作。
  deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void  // 从当前选定的内容向后删除编辑器中的内容。
  deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void //  从当前选定的内容向钱删除编辑器中的内容。
  deleteFragment: () => void //删除当前选定内容
  insertBreak: () => void //插入换行符
  insertFragment: (fragment: Node[]) => void //在当前选定内容除插入一个片段
  insertNode: (node: Node) => void //插入节点
  insertText: (text: string) => void //插入文本
  removeMark: (key: string) => void //删除标记
}
节点
一个完整的slate节点树:

const editor = {
  children: [
    {
      type: 'paragraph',
      children: [
        {
          text: 'A line of text!',
        },
      ],
    },
  ],
}
slate的节点共有三种类型:
Editor:文档树的根节点。Element:children属性,可以作为其他Node的父节点Text:是树的叶子节点,包含文本信息。
用户可以自行扩展Element和text类型的属性来扩展节点,例如增加type属性表示Node的类型(link,paragraph),增加style属性表示文本的样式(bold,color)等
type Node = Editor | Element | Text;
interface Editor{
    children:Node[]
}
interface Element{
    children:Node[]
    [key:string]:unknown
}
interface Text{
    text:string,
    [key:string]:unknown
}
Node

Node是slate中最基础的抽象。
Node对象基本属性属性:
- 
key:节点在当前文档中的索引
 - 
data:节点绑定的数据
 - 
nodes:节点的子孙
 - 
object:节点类型
 - 
text:这是一个计算属性,返回节点的文本内容
 
Element

节点类型:
- Document Element:表示编辑器的整个文档树
 - Block Element: 表示编辑器中的块级元素
 - Inline Element:表示编辑器中的行内元素
 
节点的基本属性:
- 
key:节点在当前文档中的索引
 - 
data:节点绑定的数据
 - 
nodes:节点的子孙
 - 
object:节点类型
 - 
text:一个计算属性,返回节点的文本内容
 
Text

Text Model 的基本属性:
- key:节点在当前文档中的索引
 - object:节点类型
 - text:一个计算属性,返回节点的文本内容
 - leaves: 文本叶子节点,不同格式(例如加粗,斜体等)的文本,将会被分拆为若干个 leaf
 - marks:文本节点所包含的所有 mark(标记)
 
Slate.js 是通过 mark 来标记文本格式,在视图层,开发者可以通过 CSS 或者 这样的 tag 来展示格式化文本。
Slate.js 根据 mark 类型的不同,将 Text Node 拆分为了若干 Leaf。每个 Leaf 对象含有这些属性:
- 
text: string:leaf 的文本内容。 - 
mark: Mark:leaf 被标记上的 mark。 
关于判断Text、Element:
text属性优先级更高,当同时存在text、children属性时会被判定为Text节点。
{ type: 'button', text: '123', children: [], },
定位
path

节点路径,相对于根节点的相对位置路径。
/**
 * `Path` arrays are a list of indexes that describe a node's exact position in
 * a Slate node tree. Although they are usually relative to the root `Editor`
 * object, they can be relative to any `Node` object.
 */
export type Path = number[]
例子
const editor = {
    children: [
        // Path: [0]
        {
            type: 'paragraph',
            children: [
                // Path: [0, 0]
                {
                    text: 'A line of text!',
                },
                // Path: [0, 1]
                {
                    text: 'Another line of text!',
                    bold: true,
                },
            ],
            },
		// Path: [1]
		{
			type: 'paragraph',
			children: [
				// Path: [1, 0]
				{
					text: 'A line of text!',
				},
			],
		},
    ],
}
point
定位单一字符的位置。先用path表示字符节点,再用offset表示字符在节点的位置。
/**
 * `Point` objects refer to a specific location in a text node in a Slate
 * document. Its path refers to the location of the node in the tree, and its
 * offset refers to the distance into the node's string of text. Points can
 * only refer to `Text` nodes.
 */
export interface BasePoint {
  path: Path
  offset: number
}
例子
const editor = {
    children: [
        {
            type: 'paragraph',
            children: [
                {
                    //  "!" is { path: [0, 0], offset: 14 }
                    text: 'A line of text!',
                },
            ],
        },
		{
			type: 'paragraph',
			children: [
				{
					// The point of the character "l" is { path: [1, 0], offset: 2 }
					text: 'A line of text!',
				},
			],
		},
    ],
  
}
同时提供一些静态方法:
- 比较方法
 - 检查方法
 
选区(selection)
Slate.js 中的 Selection 遵循了现代浏览器的设计,一个 Selection 对象不再允许含有多个 Range 对象,它含有属性:
anchor:选区起点focus: 选区终点isFocused: 当前选区是否被聚焦marks:当前选区包含的文本格式(有助于我们实现格式刷/清除格式等功能)

range
选取区间。分别用anchor, focus选取区间的开始位置和结束位置。
/**
 * `Range` objects are a set of points that refer to a specific span of a Slate
 * document. They can define a span inside a single node or a can span across
 * multiple nodes.
 */
export interface BaseRange {
  anchor: Point
  focus: Point
}
例子
// mynameisbhwa233
{
	anchor: {
		path: [0, 0],
		offset: 0,	
	},
	focus: {
		path: [0, 0],
		offset:5,
	},
}
// 表示选取内容为:myname
同时提供一些静态方法用来扩展选区信息:
end与start:如果说anchor/focus是 range 的「事实」起终点,那么start/end则是 range 的「视觉」起/终点,start总在end之前(或者二者重叠)isBackward与isForward:选区方向是向前还是向后isCollapsed与isExpanded:是否折叠isSet与isUnset:起点终点是否均被设置
span
用于表示没有文本的选区区间。例如选取两个图片元素。
/**
 * The `Span` interface is a low-level way to refer to locations in nodes
 * without using `Point` which requires leaf text nodes to be present.
 */
export type Span = [Path, Path]
location
Path 、 Point 、 Range 的联合类型
/**
 * The `Location` interface is a union of the ways to refer to a specific
 * location in a Slate document: paths, points or ranges.
 *
 * Methods will often accept a `Location` instead of requiring only a `Path`,
 * `Point` or `Range`. This eliminates the need for developers to manage
 * converting between the different interfaces in their own code base.
 */
export type Location = Path | Point | Range
refs
slate通过refs来指向某个节点(类似于React Refs)的定位。当节点更新时,对应的定位会跟着变化。
export interface PathRef {
  current: Path | null 
  affinity: 'forward' | 'backward' | null
  unref(): Path | null
}
export interface PointRef {
  current: Point | null
  affinity: 'forward' | 'backward' | null
  unref(): Point | null
}
export interface RangeRef {
  current: Range | null
  affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
  unref(): Range | null
}
current:节点定位。affinity:作为执行opeeration时transform函数的参数。unref:取消指向。
设置refs
export interface EditorInterface {
	pathRef: (
    editor: Editor,
    path: Path,
    options?: {
      affinity?: 'backward' | 'forward' | null
    }
  ) => PathRef
	pointRef: (
    editor: Editor,
    point: Point,
    options?: {
      affinity?: 'backward' | 'forward' | null
    }
  ) => PointRef
	rangeRef: (
    editor: Editor,
    range: Range,
    options?: {
      affinity?: 'backward' | 'forward' | 'outward' | 'inward' | null
    }
  ) => RangeRef
}
获取refs
export interface EditorInterface {
	pathRefs: (editor: Editor) => Set<PathRef>
	pointRefs: (editor: Editor) => Set<PointRef>
	rangeRefs: (editor: Editor) => Set<RangeRef>
}
operation
operation 是slate中最基础的核心操作(即原子操作),对编辑器的一切修改都是通过一个或多个opertaion来实现的。
类型
operation可以分为三类:
Node
负责与节点(node)相关的操作:
insert_node:插入节点merge_node:合并节点move_node:移动节点remove_node:删除节点set_node:设置节点属性split_node:拆分节点
Selection
负责与选区(selection)相关的操作:
set_selection:设置选区
Text
负责与纯文字相关的操作:
- 
insert_text:插入文本 - 
remove_text:删除文本 
apply
operation是通过editor.apply()调用。
例子:
editor.apply({
  type: 'insert_text',
  path: [0, 0],
  offset: 15,
  text: 'A new string of text to be inserted.',
})
editor.apply({
  type: 'remove_node',
  path: [0, 0],
  node: {
    text: 'A line of text!',
  },
})
editor.apply({
  type: 'set_selection',
  properties: {
    anchor: { path: [0, 0], offset: 0 },
  },
  newProperties: {
    anchor: { path: [0, 0], offset: 15 },
  },
})
apply()的工作流程:
Normalizing
slate规范化是通过一组完整的FLUSHING搭配一次Normalize。
为了确保slate能够正确的解析,slate有一些约束,针对这些约束也会做一些操作来保证规范化:
- 
所有的
Element节点内必须至少一个Text子节点。如果遇到不符合规范的节点,会自动加入一个空的Text节点。原因:为了确保编辑器的
selection能够选中空元素。 - 
会将相邻且属性相同的
text节点合并成一个节点。原因:为了防止编辑器内的
text节点在新增、删除文字属性时造成节点无意义的拆分。 - 
块节点的子节点(children)只能是块元素(Block)、行内块状元素(inline-block)、text节点(inline)的一种。例如
paragraph节点的子节点不能既有paragraphblock节点,还有textinline节点。slate会以子节点的第一个节点作为判断可接受类别的节点,删除其他不符合规范的子节点。原因:为了让拆分块节点相关的功能保持稳定的结果。
 - 
内联节点现在总是被文本节点包围。如果没有,slate会自动插入空的
Text节点。原因:优化编辑器的内容结构。
 - 
第一层节点只能是
Block节点,其他类型的节点会被直接删除。原因:确保编辑器存在Block节点,确保拆分节点功能正常。
 
const initialValue: Descendant[] = [
  //是block节点,正常。
  {
    type: 'paragraph',
    children: [
      { text: 'This is editable plain text, just like a <textarea>!' },
      {
        type: 'link',
        url: 'www.baidu.com',
        text: '123',
      },
    ],
  },
  // 是text节点,会被直接删除。
  { text: 'This is editable plain text, just like a <textarea>!' }, 
];
自定义规范化
规范化是通过editor 里的 normalizeNode()来实现 , 如果需要进行定制化,可以通过插件对normalizeNode 进行重写。但需要注意几点:
normalizing是重复执行的
slate是通过递归实现对内容深度遍历,即会从子节点开始normalizing再到父节点逐级进行规范化。
避免对无子节点的节点进行规范化
slate在normalizeNode前会遍历节点,没有子节点的节点会自动加入一个空的Text作为子节点。
避免无法满足约束
应避免自定义的约束,在修正后仍无法满足约束,导致无限循环normalizeNode。
运作流程
执行步骤
- customCommand
 - Transform.xxx(editor, …)
 - editor.apply(operation)
 - 重新生成 model
 - React 渲染
 
内部完整流程
- 通过
Transform的api触发编辑器更新,执行多次opertaion。 - 第一次的
opertaion除了会执行transform与normalize之外,也会将FLUSHING設為true,并将onChange的执行以 Promise 的 Micro-Task 包装起来。 opertaion通过getDirtyPath取得并更新到DIRTY_PATHSWeakMap variable。opertaion再通过GeneralTransforms.transform和Immer Draft State调用applyToDraft更新children与selection。- 执行
Transform的normalize与normalizeNode实现对脏路径的节点规范化,调用Transform來更新节点以满足约束并重跑一次相同的 Transform 流程。 - 完成所有同步更新后,执行Micro-Task的内容将 
FLUSHING设为false并触发onChange。 

Transforms
一个 transform是多个 operation 组成。一般开发中使用高阶(High-level) 的 Transformapi 来替代 低阶(Low-level) 的 operation。