Skip to content

Commit

Permalink
25/01/19
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Jan 19, 2025
1 parent bbef689 commit 6100f8b
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ jobs:
```

## 最后
## 总结
在这里我们聊了为什么要用`Monorepo`以及简单聊了一下`pnpm workspace`的优势,然后解决了在子项目开发中会遇到的`TS`编译、项目编译的两个实际问题,分别在`Monorepo`、`Rspack`、`Webpack`项目中相关的部分实践了一下,最后还简单聊了一下利用`GitHub Action`直接在`Git Pages`部署在线`DEMO`。那么再往后边的文章中,我们就需要聊一聊如何实现 层级渲染与事件管理 的能力设计。

## 每日一题
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ class ElementNode extends Node {
}
```

## 最后
## 总结
在这里我们聊了聊如何抽象基本的图形绘制以及状态的管理,因为我们的需求在这里所以我们的图形绘制能力会设计的比较简单,而状态管理则是迭代了三个方案才确定通过轻量`DOM`的方式来实现,那么再往后,我们就需要聊一聊如何实现 层级渲染与事件管理 的能力设计。


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export class Root extends Node {
```
## 最后
## 总结
在这里我们依旧关注在`Canvas`相关的内容设计上,聊到了在我们先前实现的轻量级`DOM`基础上如何管理事件的调度顺序以及多层级渲染的能力设计,且对于整体渲染与事件的调度节点顺序实现了数据缓存,模拟了事件的捕获与冒泡的调度,并且简单聊到了如何按需渲染的问题。那么接下来,我们会聊到对于`Canvas`画布的焦点实现以及无限画布的相关内容,并且由于当前我们已经基于`DOM`模拟实现了事件与模拟系统,我们还可以继续聊一聊节点的拖拽、选中、框选、`Resize`、以及参考线相关的问题,在我们聊完整个系统的图形插件化设计之后,我们还可以研究一下如何在`Canvas`上绘制富文本能力。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export class SelectNode extends Node {
}
```

## 最后
## 总结
在这里我们就依然在轻量级`DOM`的基础上,讨论了`Canvas`中描边与填充的绘制问题,以及`inside stroke`的实现方式,然后我们实现了基本的选中绘制以及拖拽多选的交互方案,并且实现了`Hover`的效果,以及拖拽节点的移动。那么在后边我们可以聊一下`fillRule`规则设计、按需绘制图形节点,也可以聊到更多的交互方案,例如`Resize`的交互方案、参考线能力的实现、富文本的绘制方案等等。

## 每日一题
Expand Down
2 changes: 1 addition & 1 deletion Backup/Slate文档编辑器-Decorator装饰器.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ for (let i = 0; i < leaves.length; i++) {
}
```

## 最后
## 总结
在这里我们主要讨论了`slate`中的`decoration`装饰器的实现,以及在实际使用中可能会遇到的问题,主要是在跨节点的情况下,我们需要将`range`拆分为多个`range`,然后分别进行处理,并且还分析了源码来探究了相关问题的实现。那么在后边的文章中我们就主要聊一聊在`slate``Path`的表达,以及在`React`中是如何控制其内容表达与正确维护`Path`路径与`Element`内容渲染的方案。

## 每日一题
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const isTextBlock = (editor: Editor, node: Node): node is TextBlockElemen
};
```
## 最后
## 总结
在这里我们更专注于文档编辑器的数据结构设计,聊了聊基于`slate`实现的文档编辑器类型系统。在`slate`中还有很多额外的概念和操作需要关注,例如`Range``Operation``Editor``Element``Path`等,那么在后边的文章中我们就主要聊一聊在`slate``Path`的表达,以及在`React`中是如何控制其内容表达与正确维护`Path`路径与`Element`内容渲染的,并且我们还可以聊一聊表格模块的设计与实现。
## 每日一题
Expand Down
115 changes: 112 additions & 3 deletions Backup/从零设计实现富文本编辑器.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ export interface Op {

本着学习的目的,自然要选择开源产品多的实现,这样遇到问题可以更好地借鉴和分析相关内容。因此我同样打算选择基于`ContentEditable`,实现数据驱动的标准`MVC`模型的富文本编辑器,基于这种方式来与浏览器交互,实现基本的富文本编辑能力。在此之前,我们还是先了解一下基本的编辑器实现:

### execCommand
如果我们仅仅需要最基本的行内样式,例如加粗、斜体、下划线等,这可能在一些基本输入框中是足够的,那么我们自然可以选择使用`execCommand`来实现。甚至直接基于`execCommand`的好处就是,其体积会非常小,例如[pell](https://github.com/jaredreich/pell)的实现,仅仅需要`3.54KB`的代码体积。
### ExecCommand
如果我们仅仅需要最基本的行内样式,例如加粗、斜体、下划线等,这可能在一些基本输入框中是足够的,那么我们自然可以选择使用`execCommand`来实现。甚至直接基于`execCommand`的好处就是,其体积会非常小,例如 [pell](https://github.com/jaredreich/pell) 的实现,仅仅需要`3.54KB`的代码体积,此外还有 [react-contenteditable](https://github.com/lovasoa/react-contenteditable) 等实现

我们也可以实现可以加粗的最小`DEMO``execCommand`命令可以在`contenteditable`元素中选区内的元素执行,`document.execCommand`方法接受三个参数,分别是命令名称、显示用户界面、命令参数。显示用户界面一般都是`false``Mozilla`没有实现,而命令参数则是可选的,例如超链接命令则需要传递具体链接地址。

Expand Down Expand Up @@ -256,6 +256,8 @@ export interface Op {
- 在同时存在两行文本的时候,如果同时选中两行内容再执行`("formatBlock", false, "P")`命令,在`Chrome`中的表现是会将两行内容包裹在同个`<p>`中,而在`FireFox`中的表现则是会将两行内容分别包裹`<p>`标签。
- ...

此外还有类似于实现加粗的功能,我们无法控制是使用`<b></b>`来实现加粗还是`<strong></strong>`来实现加粗。还有浏览器的兼容性问题,例如在`IE`浏览器中是使用`<strong></strong>`来实现加粗,在`Chrome`中是使用`<b></b>`来实现加粗,`IE``Safari`不支持通过`heading`命令来实现标题命令等等。且对于一些比较复杂的功能,例如图片、代码块等等,是无法很好实现的。

当然,默认的行为并不是完全没有用的,在某些情况下,我们可能要实现纯`HTML`的编辑器。毕竟如果在基于`MVC`模式的编辑器实现中,会处理掉对`Model`来说无效的数据内容,这样就导致原本的`HTML`内容丢失,因此在这种需求背景下依赖浏览器的默认行为可能是最有效的,这种情况下我们可能主要关注的就是`XSS`的处理了。

### ContentEditable
Expand All @@ -265,10 +267,114 @@ export interface Op {
data:text/html,<div contenteditable style="border: 1px solid black"></div>
```

那么通过`document.execCommand`来执行命令修改`HTML`的方案虽然简单,我们也聊过了其可控性很差。除了上述的`execCommand`命令执行兼容性问题之外,还有很多`DOM`上的需要兼容处理的行为,例如下面存在简单加粗格式的句子:

```md
123**456**789
```

有许多方式可以表达这样的内容,编辑器可以认为显示效果是等价的,此时可能也需要对此类`DOM`结构等同处理:

```html
<span>123<b>456</b>789</span>
<span>123<strong>456</strong>789</span>
<span>123<span style="font-weight: bold;">456</span>789</span>
```

但是这里仅仅是视觉上相等,将其完整对应到`Model`上时,自然会是件麻烦的事。除此之外,选区的表达同样也是复杂的问题,以下面的`DOM`结构为例:

```html
<span>123</span><b><em>456</em></b><span>789</span>
```

如果我们要表达选区折叠在`4`这个字符左侧时,同样会出现多种表达可以实现这个位置,这实际上就会很依赖浏览器的默认行为:

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

因此为了更强的扩展以及可控性,也解决数据与视图无法对应的问题,`L1`的富文本编辑器使用了自定义数据模型的概念。即在`DOM`树的基础上抽离出来的数据结构,相同的数据结构可以保证渲染的`HTML`也是相同的,配合自定义的命令直接控制数据模型,最终保证渲染的`HTML`文档的一致性。对于选区的表达,则需要根据`DOM`选区来不断`normalize`选区`Model`

其实这也就是我们常见的`MVC`模型,当执行命令时会修改当前的模型,进而表现到视图的渲染上。简单来说就是构建一个描述文档结构与内容的数据模型,并且使用自定义的`execCommand`对数据描述模型进行修改。在这个阶段的富文本编辑器,通过抽离数据模型,解决了富文本中脏数据、复杂功能难以实现的问题。我们也可以大概描述流程:

```html
<script>
const editor = {
// Model 选区
selection: {},
execCommand: (command, value) => {
// 执行具体的命令, 例如 bold
// 命令执行后, 更新 Model 以及 调用 DOM 渲染
},
}
const model = [
// 数据模型
{ type: "bold", text: "123" },
{ type: "span", text: "123123" },
];
const render = () => {
// 根据 type 渲染具体 DOM
};
document.addEventListener("selectionchange", () => {
// 选区变换时
// 根据 dom 选区来更新 model 选区
});
</script>
```

而类似这种方案,无论是 [quill](https://github.com/slab/quill) 还是 [slate](https://github.com/ianstormtaylor/slate) 都是类似的调度。而类似于`slate`的实现,通过适配器来连接`React`之后,就需要更复杂的兼容处理。在`React`节点中加入`ContentEditable`后,会出现类似下面的`warning`:

```js
<div
contentEditable
suppressContentEditableWarning
></div>
// A component is `contentEditable` and contains `children` managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.
```

这个`warning`的意思是,`React`无法保证`ContentEditable`中的`children`不会被意外修改或复制,这可能是不被意料到的。也就是说除了`React`本身是会需要执行`DOM`操作的,使用了`ContentEditable`之后,这个行为就变的不受控了,自然这个问题同样会出现在我们的编辑器中。

那么其实我们是可以避免使用`ContentEditable`的,设想一下即使我们没有实现编辑器,同样是可以选择页面上的文本内容的,就是我们普通的选区实现。那么如果借助原生的选区实现,然后在此基础上实现控制器层,就可以实现完全受控的编辑器。

但是这里存在一个很大的问题,就是内容的输入,因为不启用`ContentEditable`的话是无法出现光标的,自然也无法输入内容。而如果我们想唤醒内容输入,特别是需要唤醒`IME`输入法的话,浏览器给予的常规`API`就是借助`<input>`来完成,因此我们就必须要实现隐藏的`<input>`来实现输入。

但是使用隐藏的`<input>`就会出现其他问题,因为焦点在`input`上时,浏览器的文本就无法选中了。因为在同个页面中,焦点只会存在一个位置,因此在这种情况下,我们就必须要自绘选区的实现了。例如钉钉文档、有道云笔记就是自绘选区,开源的 [TextBus](https://github.com/textbus/textbus) 同样采取了这种实现。

在这里可以总结一下,使用`ContentEditable`需要处理很多`DOM`的特异行为,但是明显我们是不需要太过于处理唤醒输入这个行为。而如果不使用`ContentEditable`,却使用`DOM`来呈现富文本内容,则必须要借助额外的隐藏`input`节点来实现输入,由于焦点问题在这种情况下就不能使用浏览器的选区行为,因此就需要自绘选区的实现。


### Canvas
排版引擎
基于`Canvas`绘制我们需要的内容,颇有些文艺复兴的感觉,这种实现方式是完全不依赖`DOM`的,因此可以完全控制排版引擎。那么文艺复兴指的是,基于`DOM`兼容实现的任何生态都会失效,例如无障碍、`SEO`、开发工具支持等等。

那么为什么要抛弃现有的`DOM`生态,转而用`Canvas`来绘制富文本内容。特别是富文本会是非常复杂的内容,因为除了文本外,还有图片的内容,以及很多结构话格式的内容,例如表格等。这些内容都需要自行实现,那么在`Canvas`中实现这些内容其实相当于重新实现了部分`skia`

基于`Canvas`绘制的编辑器,当前主要有腾讯文档、`Google Doc`等,而开源的编辑器实现有 [Canvas Editor](https://github.com/Hufe921/canvas-editor)。而除了文档编辑器之外,在线表格的实现基本都是`Canvas`实现,例如腾讯文档`Sheet`、飞书多维表格等,开源的实现有 [LuckySheet](https://github.com/dream-num/Luckysheet)

`Google Doc`发布的`Blog`中,对于使用`Canvas`绘制文档主要选了两个原因:

- 文档的一致性: 这里的一致性指的是浏览器对于类似行为的兼容,举个例子: 在`Chrome`中双击某段文本的内容,选区会自动选择为整个单词,而在早期`FireFox`中则是会自动选择一句话。类似这种行为的不一致会导致用户体验的不一致,而使用`Canvas`绘制文档可以自行实现保证这种一致性。
- 高效的绘制性能: 通过`Canvas`绘制文档,可以更好地控制绘制时机,而不需要等待重绘回流,更不需要考虑`DOM`本身复杂的兼容性考量,以此造成的性能损失。此外,`Canvas`绘图替代繁重的`DOM`操作,通过逐帧渲染和硬件加速可以提升渲染性能,从而提高用户操作的响应速度和整体使用体验。

此外,排版引擎还可以控制文档的排版效果,做富文本的各种需求,我们就可能面临产品为什么不能支持像`office word`那样的效果。例如如果我们编写的文字正好排满了一行,假如在这里再加一个句号,那么前边的字就会挤一挤,从而可以使这个句号是不需要换行。而如果我们再敲一个字的话,这个字是会换行的。在浏览器的排版中是不会出现这个状态的,所以假如需要突破浏览器的排版限制,就需要自己实现排版能力。

```html
<!-- word -->
文本文本文本文本文本文本文本文本文本文本文本文本文本文本。
<!-- 浏览器 -->
文本文本文本文本文本文本文本文本文本文本文本文本文本文本
```

也就是说,在`word`中通常是不会出现句号在段落起始的,而在浏览器中是会存在这种情况的,特别是在纯`ASCII`字符的情况下。如果想规避这种排版状态的差异,就必须要自行实现排版引擎,以此来控制文档的排版效果。

此外,还有一些其他的功能,例如受控的`RTL`布局、分页、页码、页眉、脚注、字体字形控制等等。特别是分页的能力,在某些需要打印的情况下,这个效果是很必要的,但是`DOM`的实现在绘制前是无法得知其高度的,因此也就无法很好地实现分页的效果。除此之外,还有大表格的分页渲染效果等等,都变得难以控制。

因此,这些如果希望对齐`word`的实现,就必须要用`Canvas`从头造一遍。除了这些额外的功能,还有原本的浏览器基于`DOM`实现的基本功能,例如输入法的支持、复制粘贴的支持、拖拽的支持等等。而基本的`Canvas`是无法支持这些功能的,特别是输入法`IME`的支持,以及文本选区的实现,都需要很复杂的交互实现,这样的成本自然不会是很容易接受的。

## 总结


## 每日一题

Expand All @@ -286,4 +392,7 @@ data:text/html,<div contenteditable style="border: 1px solid black"></div>
- <https://juejin.cn/post/6974609015602937870>
- <https://github.com/yoyoyohamapi/book-slate-editor-design>
- <https://github.com/grassator/canvas-text-editor-tutorial>
- <https://www.zhihu.com/question/459251463/answer/1890325108>
- <https://www.oschina.net/translate/why-contenteditable-is-terrible>
- <https://drive.googleblog.com/2010/05/whats-different-about-new-google-docs.html>
- <https://cdacamar.github.io/data%20structures/algorithms/benchmarking/text%20editors/c++/editor-data-structures/>
Loading

0 comments on commit 6100f8b

Please sign in to comment.