🧲slate架构
type
status
date
slug
summary
tags
category
icon
password
Blocking
Blocked by
top
URL
Sub-item
Parent item
架构设计
slate 作为一个编辑器框架,提供了 Web 富文本编辑器的底层能力,并不是开箱即用的,需要自己二次开发许多内容。
slate 仓库下包含四个 package:
- slate:这一部分是编辑器的核心,定义了数据模型(model),操作模型的方法和编辑器实例本身
- slate-history:以插件的形式提供 undo/redo 能力,本文后面将会介绍 slate 的插件系统设计
- slate-react:以插件的形式提供 DOM 渲染和用户交互能力,包括光标、快捷键等等
- slate-hyperscript:让用户能够使用 JSX 语法来创建 slate 的数据。
slate (model)
slate作为内核,定义了编辑器最基本的功能:
- 定义编辑器的数据模型、包括内容的数据结构、光标和选区。
- 提供创建编辑器实例对象的方法。
- 模型的基本操作
- 插件机制
- 合法性校验
操作流程
- 通过
Transforms
提供的一系列方法生成Operation
Operation
进入 apply 流程- 记录变更脏区
- 对
Operation
进行 transform - 对 model 正确性进行校验
- 触发变更回调
Operation 修改
Slate通过
Transforms
所提供的一系列方法生成 Operation
,这些方法大致分成四种类型:GeneralTransforms
是原子化操作,只有它能直接修改 model,其他 transforms 最终都是基于 GeneralTransforms
的操作集合。GeneralTransforms
也即是 Operation
类型仅有 9 个:insert_node
:插入一个 Node
insert_text
:插入一段文本
merge_node
:将两个 Node 组合成一个
move_node
:移动 Node
remove_node
:移除 Node
remove_text
:移除文本
set_node
:设置 Node 属性
set_selection
:设置选区位置
split_node
:拆分 Node
例如:
Transforms.insertText
就是就是insert_text
与set_selection
两个操作的封装,set_selection
调用了Transforms
的其他操作,插入文本就是最后生成了一个 type
为 insert_text
的 Operation
并调用 Editor
实例的 apply
方法。apply就是用来直接操作model,即修改
editor.children
和 editor.selection
属性。slate 使用了 immer 来应用 immutable data,即
createDraft
finishDrag
成对的调用。使用 immer 可以将创建数据的开销减少到最低,同时又能使用 JavaScript 原生的 API 和赋值语法。合法性校验
对 model 进行变更之后还需要对 model 的合法性进行校验,避免内容出错。校验的机制有两个重点,一是对脏区域的管理,一个是
withoutNormalizing
机制。许多 transform 在执行前都需要先调用
withoutNormalizing
方法判断是否需要进行合法性校验:- 所有
Element
节点最后必须包含至少一个Text
节点。
- 两个相邻的有同样属性的文本会被合并。
- 块节点要么只能包含其他块节点,要么包含内联节点和文本节点。
- 行内节点既不能是父块节点的第一个或最后一个子块,也不能挨着子数组中的另一个行内节点。
- 顶级的编辑器节点只能包含块节点。
插件机制
原理:覆写编辑器实例 editor 上的方法。
例如:slate-react 提供的 withReact 方法给我们做了一个很好的示范:
用 withReact 修饰编辑器实例,直接覆盖实例上原本的 apply 和 change 方法。
slate-history(undo/redo)
undo/redo实现方式
实现 undo/redo 的机制一般来说有两种:
- 存储各个时刻(例如发生变更前后)model 的快照(snapshot),在撤销操作的时候恢复到之前的快照,
优点:实现简单。
缺点:1. 较为消耗内存(有 n 步操作我们就需要存储 n+1 份数据!)。2. 使得协同编辑实现起来非常困难(比较两个树之间的差别的时间复杂度是 O(n^3),3. 网络传输开销大。
- 记录变更的应用记录,在撤销操作的时候取要撤销操作的反操作。
优点:1. 方便做协同。2. 不会占用较多的内存空间。
缺点:机制复杂,需要定义原子操作,然后实现每一个原子操作的反操作。
slate的实现方式
slate 即基于第二种方法进行实现。在
withHistory
方法中,slate-history 在 editor 上创建了两个数组用来存储历史操作:e.history
=
{ undos
:
[], redos
:
[] }
它们的类型都是
Operation[][]
,即 Operation
的二维数组,其中的每一项代表了一批操作(在代码上称作 batch), batch 可含有多个 Operation
。slate-history 通过覆写
apply
方法来在 Operation
的 apply 流程之前插入 undo/redo 的相关逻辑,这些逻辑主要包括:- 判断是否需要存储该
Operation
,诸如改变选区位置等操作是不需要 undo 的
- 判断该
Operation
是否需要和前一个 batch 合并,或覆盖前一个 batch
- 创建一个 batch 插入
undos
队列,或者插入到上一个 batch 的尾部,同时计算是否超过最大撤销步数,超过则去除首部的 batch
- 调用原来的
apply
方法
slate-react(渲染层)
原理
slate本身是框架无关的内核,需要需要与React联动,就需要将slate数据结构转换成react组件。slate 的 model 本身就是树形结构,因此只需要递归地去遍历这棵树,然后根据类型渲染成react组件就可以了。
自定义渲染元素
则通过
renderElement
和 renderLeaf
自行决定如何渲染 model 中的一个 Node
光标和选区的处理
slate 没有自行实现光标和选区,而使用了浏览器
contenteditable
的能力,contenteditable 就负责了光标和选区的渲染和事件。slate-react 会在每次渲染的时候将 model 中的选区同步到 DOM 上。键盘事件的处理
Editable
组件创建了一个 onDOMBeforeInput
函数,用以处理 beforeInput
事件,根据事件的 type
调用不同的方法来修改 model。beforeInput
事件和 input
事件的区别就是触发的时机不同。前者在值改变之前触发,还能通过调用 preventDefault
来阻止浏览器的默认行为。渲染触发
slate 在渲染的时候会向
EDITOR_TO_ON_CHANGE
中添加一个回调函数,这个函数会让 key
的值加 1,触发 React 重新渲染。slate 缺点
- 使用了 contenteditable 导致无法处理部分选区和输入事件。使用 contenteditable 后虽然不需要开发者去处理光标的渲染和选择事件,但是造成了另外一个问题:破坏了从 model 到 view 的单向数据流,这在使用输入法(IME)的时候会导致崩溃这样严重的错误。
- 对于协同编辑的支持仅停留在理论可行性上。slate 使用了
Operation
,这使得协同编辑存在理论上的可能,但是对于协同编辑至关重要的 operation transform 方案(即如何处理两个有冲突的编辑操作),则没有提供实现。