slate架构
🧲slate架构
富文本|2023-11-22|最后更新: 2024-1-27
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作为内核,定义了编辑器最基本的功能:
  1. 定义编辑器的数据模型、包括内容的数据结构、光标和选区。
  1. 提供创建编辑器实例对象的方法。
  1. 模型的基本操作
  1. 插件机制
  1. 合法性校验

操作流程

  1. 通过 Transforms 提供的一系列方法生成 Operation
  1. Operation 进入 apply 流程
    1. 记录变更脏区
    2. 对 Operation 进行 transform
    3. 对 model 正确性进行校验
    4. 触发变更回调

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_textset_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 方法判断是否需要进行合法性校验:
  1. 所有 Element 节点最后必须包含至少一个 Text 节点。
  1. 两个相邻的有同样属性的文本会被合并。
  1. 块节点要么只能包含其他块节点,要么包含内联节点和文本节点。
  1. 行内节点既不能是父块节点的第一个或最后一个子块,也不能挨着子数组中的另一个行内节点。
  1. 顶级的编辑器节点只能包含块节点。

插件机制

原理:覆写编辑器实例 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 缺点

  1. 使用了 contenteditable 导致无法处理部分选区和输入事件。使用 contenteditable 后虽然不需要开发者去处理光标的渲染和选择事件,但是造成了另外一个问题:破坏了从 model 到 view 的单向数据流,这在使用输入法(IME)的时候会导致崩溃这样严重的错误。
  1. 对于协同编辑的支持仅停留在理论可行性上。slate 使用了 Operation,这使得协同编辑存在理论上的可能,但是对于协同编辑至关重要的 operation transform 方案(即如何处理两个有冲突的编辑操作),则没有提供实现。

扩展

参考

 
工程化webpack5 模块联邦
Loading...