Skip to content

Commit

Permalink
25/02/22
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Feb 22, 2025
1 parent ea3bdf9 commit f3abb54
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 5 deletions.
83 changes: 82 additions & 1 deletion Backup/基于MVC模式的编辑器架构设计.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

从零实现富文本编辑器项目的相关文章:

- [深感缺乏擅长领域,准备试着从零开始写个富文本编辑器]()
- [深感一无所长,准备试着从零开始写个富文本编辑器]()
- [从零实现富文本编辑器-基于MVC模式的编辑器架构设计]()

## 精简的编辑器
Expand Down Expand Up @@ -278,7 +278,88 @@ Core

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

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

```html
<p data-slate-node="element">
<span data-slate-node="text">
<span data-slate-leaf="true">
<span data-slate-string="true">text</span>
</span>
</span>
</p>
```

那么我们通过`react`来构建视图层自然也会存在这样的模版结构,因此在序列化的过程中就是需要将这部分复杂的结构序列化为相对规范的`HTML`。特别是很多样式我们并不是使用规范的语义标签,而是通过`style`属性来实现的,因此将其规整化是非常重要的。

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

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

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

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

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

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

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

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

```html
<div contenteditable id="$1"></div>
<script>
const stop = (e) => {
e.preventDefault();
e.stopPropagation();
};
$1.addEventListener('beforeinput', stop);
$1.addEventListener('input', stop);
$1.addEventListener('keydown', stop);
$1.addEventListener('keypress', stop);
$1.addEventListener('keyup', stop);
$1.addEventListener('compositionstart', stop);
$1.addEventListener('compositionupdate', stop);
$1.addEventListener('compositionend', stop);
</script>
```

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

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

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

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

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

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

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

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

```js
// <span>123</span><b><em>456</em></b><span>789</span>
{ node: 123, offset: 3 }
{ node: <em></em>, offset: 0 }
{ node: <b></b>, offset: 0 }
```

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

```
|-- LeafState
|-- LineState --|-- LeafState
| |-- LeafState
BlockState --|
| |-- LeafState
|-- LineState --|-- LeafState
|-- LeafState
```

### Delta

Expand Down
4 changes: 2 additions & 2 deletions I18N/Plugin/基于Chrome扩展的浏览器事件.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Of course, we have just replaced the requirements here, as mentioned earlier, th
<div class="text-block">
<div class="zone-container text-editor non-empty" data-zone-id="2" data-zone-container="*" data-slate-editor="true" contenteditable="true">
<div class="ace-line" data-node="true" dir="auto">
<span data-string="true" class=" author-0087753711195911211" data-leaf="true">123</span>
<span data-string="true" style="font-weight:bold;" class=" author-0087753711195911211" data-leaf="true">123</span>
<span data-string="true" class=" author-x" data-leaf="true">123</span>
<span data-string="true" style="font-weight:bold;" class=" author-x" data-leaf="true">123</span>
<span data-string="true" data-enter="true" data-leaf="true">&ZeroWidthSpace;</span>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions Plugin/基于Chrome扩展的浏览器事件.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
<div class="text-block">
<div class="zone-container text-editor non-empty" data-zone-id="2" data-zone-container="*" data-slate-editor="true" contenteditable="true">
<div class="ace-line" data-node="true" dir="auto">
<span data-string="true" class=" author-0087753711195911211" data-leaf="true">123</span>
<span data-string="true" style="font-weight:bold;" class=" author-0087753711195911211" data-leaf="true">123</span>
<span data-string="true" class=" author-x" data-leaf="true">123</span>
<span data-string="true" style="font-weight:bold;" class=" author-x" data-leaf="true">123</span>
<span data-string="true" data-enter="true" data-leaf="true">&ZeroWidthSpace;</span>
</div>
</div>
Expand Down

0 comments on commit f3abb54

Please sign in to comment.