Slate.js 介绍
Slate 是一个 L1 级的富文本编辑器框架,其架构分为四个部分:核心逻辑层slate、视图层slate-react、历史操作层slate-history以及用于复制粘贴外部内容的 helper slate-hyperscript。
本文的主题是 slate 层中的一个概念,Operation。
Operation
Operation 是 Slate 中进行变换的粒度最小的原子操作,所有的高级变换(Transforms API)归根结底其实都只是一个或者多个 Operation 的封装打包。
Operation 能够帮助我们实现历史操作(撤销/重做)、协同编辑等功能。如此重要的内容,却缺乏官方的 API 文档作为参考,由是撰写本文。
规范
Opertaion 原子操作,它是一个对象,该对象必须包含属性 type。
type 的取值为以下 Slate 中支持的总共 9 种 Operations。
其中 insert_node 和 remove_node、insert_text 和 remove_text、merge_node 和 split_node 三对操作互为逆运算,而余下的三者的逆运算为自身。
逆运算的意思是,两个 Operation 执行的操作得到的效果是完全相反的。
互为逆操作
举个例子,我想“插入文本”——在 { path:[0], offset: 0} 这个 Point 插入一个字符串 "hello"。
e.apply(
{type: 'insert_text', path: [0], offset: 0, "hello"}
)
按照对于上图的理解,将 insert_text 改成逆操作 remove_text,以下代码可撤销掉“插入文本”。
e.apply(
{type: 'remove_text', path: [0], offset: 0, "hello"}
)
Slate 本身提供了 Operation.inverse(op) 的方法。输入、输出均是 Operation,可用它替我们撤销“插入文本”。
e.apply(
Operation.inverse(
{type: 'insert_text', path: [0], offset: 0, "hello"}
)
)
源码中,
Operation.inverse(op)对于type为insert_text的情形,所做的事情也不过是将type用remove_text替换,然后返回这个 Operation 出来。
逆运算为自身
对于逆运算为自身的情况,move_node、set_node 和 set_selection。
// 移动节点
e.apply(
{type: 'move_node', path: [1], newPath: [0]}
)
// 逆运算如下
e.apply(
{type: 'move_node', path: [0], newPath: [1]}
)
// 设置节点属性
e.apply(
{type: 'set_node', path: [0], properties: {lineHeight: 1.75}, newProperties: {lineHeight: 2}}
)
// 逆运算如下
e.apply(
{type: 'set_node', path: [0], properties: {lineHeight: 2}, newProperties: {lineHeight: 1.75}} )
// 设置选区属性
e.apply(
{type: 'set_selection', properties: null, newProperties: {bold: true}}
)
// 逆运算如下
e.apply(
{type: 'set_selection', properties: {bold: true}, newProperties: null}
)
move_node、set_node 和 set_selection 三者有一个共同的特点,就是 Operation 中含有一个 X,同时又存在一个 newX。当他们取逆运算的时候,只需要将 X 与 newX 对调即可。
同样,你可以并且应该使用 Operation.inverse(op) 更高效地实现逆运算。
e.apply(
Operation.inverse(
{type: 'set_selection', properties: null, newProperties: {bold: true}}
)
)
上述仅描述 Operation 作为一个接口时体现的特性,具体涉及到在 editor 中应用某个操作的逻辑,可以参考transform。
应用
历史操作
由于 Operation 支持互逆操作的特性,因此可以将它们以数组的形式存储起来,用来保存历史操作。
如 slate-history 中提供的 HistoryEditor 包装了两个数组(undo 和 redo) 到 editor 实例上。
e.history = {undo: [], redo: []}
协同编辑
Slate 的协同是基于交换可复制数据类型(CRDT, Commutative Replicated Data Types),目前的技术包括slate-collaborative 以及 Yjs。
Operation 提供了最小粒度的原子操作,而至于操作之间如何优雅地(即保留作者意图的信息)合并在一起,由这篇译文讲解算法相关的逻辑。
参考资料