Slate.js 之 Operation 概述

1,746 阅读3分钟

Slate.js 介绍

Slate 是一个 L1 级的富文本编辑器框架,其架构分为四个部分:核心逻辑层slate、视图层slate-react、历史操作层slate-history以及用于复制粘贴外部内容的 helper slate-hyperscript

image.png

本文的主题是 slate 层中的一个概念,Operation。

Operation

Operation 是 Slate 中进行变换的粒度最小的原子操作,所有的高级变换(Transforms API)归根结底其实都只是一个或者多个 Operation 的封装打包。

Operation 能够帮助我们实现历史操作(撤销/重做)、协同编辑等功能。如此重要的内容,却缺乏官方的 API 文档作为参考,由是撰写本文。

规范

Opertaion 原子操作,它是一个对象,该对象必须包含属性 type

type 的取值为以下 Slate 中支持的总共 9 种 Operations。

image.png

其中 insert_noderemove_nodeinsert_textremove_textmerge_nodesplit_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) 对于 typeinsert_text 的情形,所做的事情也不过是将 typeremove_text 替换,然后返回这个 Operation 出来。

逆运算为自身

对于逆运算为自身的情况,move_nodeset_nodeset_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_nodeset_nodeset_selection 三者有一个共同的特点,就是 Operation 中含有一个 X,同时又存在一个 newX。当他们取逆运算的时候,只需要将 XnewX 对调即可。

同样,你可以并且应该使用 Operation.inverse(op) 更高效地实现逆运算。

e.apply(
    Operation.inverse(
        {type: 'set_selection', properties: null, newProperties: {bold: true}} 
    )
)

上述仅描述 Operation 作为一个接口时体现的特性,具体涉及到在 editor 中应用某个操作的逻辑,可以参考transform

应用

历史操作

由于 Operation 支持互逆操作的特性,因此可以将它们以数组的形式存储起来,用来保存历史操作。

slate-history 中提供的 HistoryEditor 包装了两个数组(undoredo) 到 editor 实例上。

e.history = {undo: [], redo: []}

协同编辑

Slate 的协同是基于交换可复制数据类型(CRDT, Commutative Replicated Data Types),目前的技术包括slate-collaborative 以及 Yjs

Operation 提供了最小粒度的原子操作,而至于操作之间如何优雅地(即保留作者意图的信息)合并在一起,由这篇译文讲解算法相关的逻辑。

参考资料

  1. Slate.js - 革命性的富文本编辑框架
  2. 【长文】Web 富文本编辑器框架 slate.js - 从基本使用到核心概念
  3. 钉钉文档编辑器的前世今生