Skip to content

Commit

Permalink
25/02/23
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Feb 23, 2025
1 parent f3abb54 commit caaab89
Showing 1 changed file with 29 additions and 15 deletions.
44 changes: 29 additions & 15 deletions Backup/基于MVC模式的编辑器架构设计.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ editor.updateDOMselection();
那么我们基本编辑器`MVC`模型已经实现,因此自然而然就可以将其抽象为独立的`package`,恰好我们也是通过`monorepo`的形式来管理项目的。因此在这里就可以将其抽象为`core``delta``react``utils`四个核心包,分别对应编辑器的核心逻辑、数据模型、视图层、工具函数。而具体的编辑器模块实现,则全部以插件的形式定义在`plugin`包中。

### Core
`core`模块封装了编辑器的核心逻辑,包括剪贴板模块、历史操作模块、输入模块、选区模块、状态模块等等,所有的模块通过实例化的`editor`对象引用。这里除了本身分层的逻辑实现外,还希望实现模块的扩展能力,可以通过引用编辑器模块并且扩展能力后,可以重新装载到编辑器上。
`Core`模块封装了编辑器的核心逻辑,包括剪贴板模块、历史操作模块、输入模块、选区模块、状态模块等等,所有的模块通过实例化的`editor`对象引用。这里除了本身分层的逻辑实现外,还希望能够实现模块的扩展能力,可以通过引用编辑器模块并且扩展能力后,可以重新装载到编辑器上。

```
Core
Expand All @@ -276,7 +276,7 @@ Core
└── ...
```

实际上`core`模块中存在本身的依赖关系,例如选区模块依赖于事件模块的事件分发,这主要是由于模块在构造时需要依赖其他模块的实例,以此来初始化本身的数据和事件等。因此事件实例化的顺序会比较重要,但是我们在实际聊下来的时候则直接按上述定义顺序,并未按照直接依赖的有向图顺序。
实际上`Core`模块中存在本身的依赖关系,例如选区模块依赖于事件模块的事件分发,这主要是由于模块在构造时需要依赖其他模块的实例,以此来初始化本身的数据和事件等。因此事件实例化的顺序会比较重要,但是我们在实际聊下来的时候则直接按上述定义顺序,并未按照直接依赖的有向图顺序。

`clipboard`模块主要负责数据的序列化与反序列化,以及剪贴板的操作。通常来说,富文本编辑器的`DOM`结构并没有那么的规范,举个例子,在`slate`中我们可以看到诸如`data-slate-node``data-slate-leaf`等节点属性,我们可以将其理解为模版结构。

Expand All @@ -294,17 +294,17 @@ Core

反序列化则将`HTML`转换为编辑器的数据模型,这部分实现则是为了跨编辑器的内容粘贴。编辑器内建的数据结构通常都不一致,因此跨编辑器就需要较为规范的中间结构。这其实也是编辑器中不成文的规定,`A`编辑器序列化的时候尽可能规范,`B`编辑器反序列化才可以更好地处理。

`collect`模块则是可以根据选区数据来得到相关的数据,举个例子,当用户选中了一段文本,执行复制的时候就需要将选中的这部分数据内容取出来,然后才能进行序列化操作。此外,`collect`模块还可以取得某个位置的`op`节点、`marks`继承处理等等。
`collect`模块是可以根据选区数据来得到相关的数据,举个例子,当用户选中了一段文本,执行复制的时候就需要将选中的这部分数据内容取出来,然后才能进行序列化操作。此外,`collect`模块还可以取得某个位置的`op`节点、`marks`继承处理等等。

`editor`模块则是编辑器的模块聚合类,其本身主要是管理整个编辑器的生命周期,例如实例化、挂载`DOM`、销毁等状态。此模块需要组合所有的模块,并且还需要关注模块的有向图组织依赖关系,主要的编辑器`API`都应该从此模块暴露出来。
`editor`模块是编辑器的模块聚合类,其本身主要是管理整个编辑器的生命周期,例如实例化、挂载`DOM`、销毁等状态。此模块需要组合所有的模块,并且还需要关注模块的有向图组织依赖关系,主要的编辑器`API`都应该从此模块暴露出来。

`event`模块则是事件分发模块,原生事件的绑定都是在该模块中实现,编辑器内所有的事件都应该从该模块来分发。这种方式可以有更高度的自定义空间,例如扩展插件级别的事件执行,并且可以减少内存泄漏的概率,毕竟只要我们能够保证编辑器的销毁方法调用,那么所有的事件都可以被正确卸载。
`event`模块是事件分发模块,原生事件的绑定都是在该模块中实现,编辑器内所有的事件都应该从该模块来分发。这种方式可以有更高度的自定义空间,例如扩展插件级别的事件执行,并且可以减少内存泄漏的概率,毕竟只要我们能够保证编辑器的销毁方法调用,那么所有的事件都可以被正确卸载。

`history`模块则是维护历史操作的模块,在编辑器中实现`undo``redo`是比较复杂的,我们需要基于原子化的操作执行,而不是存储编辑器的全量数据快照,并且需要维护两个栈来处理数据转移。此外我们还需要在此基础上实现扩展,例如自动组合、操作合并、协同处理等。
`history`模块是维护历史操作的模块,在编辑器中实现`undo``redo`是比较复杂的,我们需要基于原子化的操作执行,而不是存储编辑器的全量数据快照,并且需要维护两个栈来处理数据转移。此外我们还需要在此基础上实现扩展,例如自动组合、操作合并、协同处理等。

这里的自动组合指的是用户进行高频连续操作时,我们需要将其合并为一个操作。操作合并则是指我们可以通过`API`来实现合并,例如用户上传图片后,执行了其他输入操作,然后上传成功后产生的操作,最后这个操作应该合并到上传图片的这个操作上。协同处理则是需要遵循一个原则,即我们仅能撤销属于自己的操作,而不能撤销其他人协同过来的操作。

`input`模块则是处理输入的模块,输入是编辑器的核心操作之一,我们需要处理输入法、键盘、鼠标等输入操作。输入法的交互处理是需要非常多的兼容处理,例如输入法还存在候选词、联想词、快捷输入、重音等等。甚至是移动端的输入法兼容更麻烦,在`draft`中还单独列出了移动端输入法的兼容问题。
`input`模块是处理输入的模块,输入是编辑器的核心操作之一,我们需要处理输入法、键盘、鼠标等输入操作。输入法的交互处理是需要非常多的兼容处理,例如输入法还存在候选词、联想词、快捷输入、重音等等。甚至是移动端的输入法兼容更麻烦,在`draft`中还单独列出了移动端输入法的兼容问题。

举个目前比较常见的例子,`ContentEditable`无法真正阻止`IME`的输入,这就导致了我们无法真正阻止中文的输入。在下面的这个例子中,输入英文和数字是不会有响应的,但是中文却是可以正常输入的,这也是很多编辑器选择自绘选区和输入的原因之一,例如`VSCode`、钉钉文档等。

Expand All @@ -326,19 +326,19 @@ Core
</script>
```

`model`模块则是用来映射`DOM`视图和状态模型的关系,这部分是视图层和数据模型的桥梁,在很多时候我们需要通过`DOM`来获取状态模型,同样也会需要通过状态模型在获取对应的`DOM`视图。这部分就是利用`WeakMap`来维护映射,以此来实现状态的同步。
`model`模块是用来映射`DOM`视图和状态模型的关系,这部分是视图层和数据模型的桥梁,在很多时候我们需要通过`DOM`来获取状态模型,同样也会需要通过状态模型在获取对应的`DOM`视图。这部分就是利用`WeakMap`来维护映射,以此来实现状态的同步。

`perform`模块则是封装了针对数据模型执行变更的基础模块,由于构造基本的`delta`操作会比较复杂,例如执行属性`marks`的变更,是需要过滤掉`\n`的这个`op`,反过来对行属性的操作则是需要过滤掉普通文本`op`。因此需要封装这部分操作,来简化执行变更的成本。
`perform`模块是封装了针对数据模型执行变更的基础模块,由于构造基本的`delta`操作会比较复杂,例如执行属性`marks`的变更,是需要过滤掉`\n`的这个`op`,反过来对行属性的操作则是需要过滤掉普通文本`op`。因此需要封装这部分操作,来简化执行变更的成本。

`plugin`模块则实现了编辑器的插件化机制,插件化是非常有必要的,理论上而言普通文本外的所有格式都应该由插件来实现。那么这里的插件化主要是提供了基础的插件定义和类型,管理了插件的生命周期,以及诸如按方法调用分组、方法调度优先级等能力。
`plugin`模块实现了编辑器的插件化机制,插件化是非常有必要的,理论上而言普通文本外的所有格式都应该由插件来实现。那么这里的插件化主要是提供了基础的插件定义和类型,管理了插件的生命周期,以及诸如按方法调用分组、方法调度优先级等能力。

`rect`模块则是用来处理编辑器的位置信息,在很多时候我们需要根据`DOM`节点来计算位置,并且需要提供节点在编辑器的相对位置,特别是很多附加能力中,例如虚拟滚动的视口锁定、对比视图的虚拟图层、评论能力的高度定位等等。此外,选区的位置信息也是很重要的,例如浮动工具栏的弹出位置。
`rect`模块是用来处理编辑器的位置信息,在很多时候我们需要根据`DOM`节点来计算位置,并且需要提供节点在编辑器的相对位置,特别是很多附加能力中,例如虚拟滚动的视口锁定、对比视图的虚拟图层、评论能力的高度定位等等。此外,选区的位置信息也是很重要的,例如浮动工具栏的弹出位置。

`ref`模块则是实现了编辑器的位置转移引用,这部门其实是利用了协同的`transform`来处理的索引信息,类似于`slate``PathRef`。举个例子,当用户上传图片后,此时可能会进行其他的内容插入操作,此时图片的索引值会发生变化,而使用`ref`模块则可以拿到最新的索引值。
`ref`模块是实现了编辑器的位置转移引用,这部门其实是利用了协同的`transform`来处理的索引信息,类似于`slate``PathRef`。举个例子,当用户上传图片后,此时可能会进行其他的内容插入操作,此时图片的索引值会发生变化,而使用`ref`模块则可以拿到最新的索引值。

`schema`模块则是用来定义编辑器的数据应用规则,我们需要在此处定义数据属性需要处理的方法,例如加粗的属性`marks`需要在输入后继续继承加粗属性,而行内代码`inline`类型则不需要继续继承,类似于图片、分割线则需要被定义为独占整行的`Void`类型,`Mention``Emoji`等则需要被定义为`Embed`类型。
`schema`模块是用来定义编辑器的数据应用规则,我们需要在此处定义数据属性需要处理的方法,例如加粗的属性`marks`需要在输入后继续继承加粗属性,而行内代码`inline`类型则不需要继续继承,类似于图片、分割线则需要被定义为独占整行的`Void`类型,`Mention``Emoji`等则需要被定义为`Embed`类型。

`selection`模块则是用来处理选区的模块,选区是编辑器的核心操作基准,我们需要处理选区同步、选区校正等等。实际上选区的同步是非常复杂的事情,从浏览器的`DOM`映射到选区模型本身就是需要精心设计的事情,而选区的校正则是需要处理非常多的边界情况。
`selection`模块是用来处理选区的模块,选区是编辑器的核心操作基准,我们需要处理选区同步、选区校正等等。实际上选区的同步是非常复杂的事情,从浏览器的`DOM`映射到选区模型本身就是需要精心设计的事情,而选区的校正则是需要处理非常多的边界情况。

在先前我们也提到了相关的问题,以下面的`DOM`结构为例,如果我们要表达选区折叠在`4`这个字符左侧时,同样会出现多种表达可以实现这个位置,这实际上就会很依赖浏览器的默认行为。因此这样就需要我们自己来保证这个选区的映射,以及在非常规状态下的校正逻辑。

Expand All @@ -349,7 +349,7 @@ Core
{ node: <b></b>, offset: 0 }
```

`state`模块则维护了编辑器的核心状态,在实例化编辑器时传递基本数据后,我们后续维护的内容就变成了状态,而不是最开始传递的数据内容。我们的状态变更方法同样会在此处实现,特别是`Immutable/Key`的状态维护,我们需要保证状态的不可变性,以此来减少重复渲染。
`state`模块维护了编辑器的核心状态,在实例化编辑器时传递基本数据后,我们后续维护的内容就变成了状态,而不是最开始传递的数据内容。我们的状态变更方法同样会在此处实现,特别是`Immutable/Key`的状态维护,我们需要保证状态的不可变性,以此来减少重复渲染。

```
|-- LeafState
Expand All @@ -362,15 +362,29 @@ BlockState --|
```

### Delta
`Delta`模块封装了编辑器的数据模型,我们需要基于数据模型来描述编辑器的内容,以及编辑器内容的变更。除此之外,还封装了数据模型的诸多操作,例如`compose``transform``invert``diff``Iterator`等等。

```
Delta
├── attributes
├── delta
├── mutate
├── utils
└── ...
```

这里的`Delta`实现是基于`Quill`的数据模型改造的,`Quill`的数据模型设计非常优秀,特别是封装了基于`OT`的操作变换等方法。但是设计上还是存在不方便的地方,因此参考了`EtherPad`的数据实现,在此基础上改造了部分实现,我们后续会详细讲述数据结构设计。

此外需要注意的是,我们的`Delta`实现最主要的是用来描述文档以及变更,相当于一种序列化和反序列化的实现。上边也提到了在初始化编辑器之后,我们维护的数据就变成了内建的状态,而非最初初始化的数据内容。因此很多方法在控制器层面上,都会有单独的设计,例如`immutable`的状态维护。

`attributes`模块维护了针对文本描述属性的操作,我们在这里简化了属性的实现,即`AttributeMap`类型定义了为`<string, string>`的类型。而具体的模块中则定义了`compose``invert``transform``diff`等方法,以此来实现属性的合并、反转、变换、差异等操作。

`delta`模块实现了整个编辑器的数据模型,`delta`通过`ops`实现了线形结构的数据模型。`ops`的结构主要包括三种操作,`insert`用来描述插入文本、`delete`用来描述删除文本、`retain`用来描述保留文本/移动指针,以及在此基础上的`compose``transform`等等方法。

`mutate`模块则实现了`immutable``delta`模块实现,并且独立了`\n`作为独立的`op`。最初的控制器设计实现是基于数据变更实现的,后续将其改造为原始状态的维护,因此这部分实现移动到了`delta`模块中,因此这部分可以直接对应编辑器的状态维护,可以用于单元测试等等。

`utils`模块则封装了对于`op`以及`delta`的辅助方法,`clone`的相关方法实现了诸如`op``delta`等深拷贝以及对等方法,当然由于我们新的设计则无需引入`lodash`的相关方法。此外还实现了一些数据的判断以及格式化方法,例如数据的起始/结束字符串判断、分割`\n`的方法等等。

### React

```
Expand Down

0 comments on commit caaab89

Please sign in to comment.