首页 » 网站建设 » phphprose道理技巧_Quill编辑器实现事理初探

phphprose道理技巧_Quill编辑器实现事理初探

访客 2024-12-08 0

扫一扫用手机浏览

文章目录 [+]

富文本编辑器根据实在现办法,业内将其划分为L0 ~ L2,层层递进,功能的支撑也越来越强大。

阶段

phphprose道理技巧_Quill编辑器实现事理初探

描述

phphprose道理技巧_Quill编辑器实现事理初探
(图片来自网络侵删)

范例产品

L0

视图层基于contenteditable,逻辑层基于document.execCommand,直接操作DOM

UEditor、TinyMCE

L1

视图层基于contenteditable,逻辑层对DOM进行抽象,用数据去驱动视图更新

Quill、Prosemirror、slate、Draft

L2

自己实现内容排版,不依赖于浏览器原生操作

Google Docs、WPS

L0级编辑器,基于contenteditable与document.execCommand指令,直接操作DOM,大略粗暴,所见即所得,其优点是大略,我们只须要聚焦在视图层,document.execCommand自身也供应一些操作指令,可以知足基本的文本操作需求,个性化的需求也可以通过封装自定义指令来知足;同理,缺陷也很明显,只关注视图层,没有逻辑抽象,对付操作记录,文档构造变革,是黑盒,对付文档的版本管理、协同办公之类的需求,无能为力,因此,带着痛点,孕育出了L1级编辑器。

L1级编辑器核心亮点为增加了一层DOM抽象,用数据去驱动视图的更新。
HTML是一门标记措辞,没有较强逻辑性,而且可以层层嵌套,元素的种类又分为行内元素、行内块元素、块级元素,每个元素的表现形式又有差异,删繁就简,客不雅观描述出每个元素的构造与行为,会让全体文档变得自主可控。
字符是分散在不同的DOM节点中,树形构造遍历的韶光繁芜度是O(nh),这无疑是一种巨大的性能花费,因此L1级编辑器,用一种扁平化的数据构造去描述字符的位置、样式,这样对付字符查找、字符操作,会提升不少性能,详细实现细节也是很繁芜的,后面会逐步先容。

L0、L1级编辑器,自身并没有分开DOM,底层还是依赖于contenteditable,还是受限于浏览器自身,比如页面排版、焦点、选区等。
但是到了L2级编辑器,就分开了浏览器原生操作。
利用canvas或svg来实现内容编排,焦点、选区等操作都是自技艺动去实现。
这部分过于繁芜,也只有Google、WPS之类的厂商才有实力去研发,我们不做过多的穷究。

Quill编辑器API比较大略,观点比较清晰,上手也比prosemirror大略,又有底层定制开拓能力,利用范围较广。
本文将大略先容Quill的一些核心观点和操作过程,实现细节在后续的文章中逐步先容。

Quill 基本事理

通过简介中的先容,我们知道L1级编辑器的几个核心观点,

document文档数据模型(对应Quill中的Parchment)DOM节点Node的描述(对应Quill中的Blot)一种扁平化的字符位置、样式描述(对应Quill中的Delta)

下文我们对以上Quill中的观点做进一步的描述。

核心观点Delta

套用官网的话,什么是Delta?

这段话翻译为中文为:“Deltas是一种大略而富有表现力的格式,可以用来描述Quill的内容和变革。
该格式是JSON的严格子集,是人类可读的,机器很随意马虎解析。
Deltas可以描述任何Quill文档,包括所有文本和格式信息,没有HTML的歧义和繁芜性。

一个Delta数据构造表现形式:

// 编辑器初始值{ "ops": [ { "insert": "Hello " }, { "insert": "World" }, ]}// 给World加粗后的值// 3种动作:insert: 插入,retain:保留, delete:删除{ "ops": [ { "retain": 6 }, { "retain": 5, "attributes": { "bold": true } } ]}

这个能力使文档协同编辑成为了可能。
最大略的协同编辑,通过以下几步操作即可:

监听编辑器文本改变text-change,获取数据改变的描述Delta通过websocket将Delta分发给每位协同编辑用户调用Quill实例中UpdateContents,更新协同编辑文档

Delta对付文档的位置、样式描述,极大的简化文档操作,最原始的文档查找更换,须要深度优先遍历,还须要递归查找,十分不便,有了Delta,它精准的描述了每个字符的位置,我们就可以像处理纯文本一样处理富文本。

Parchment与Blot

Parchment是document的数据抽象,而Blot是对Node节点的抽象。
也便是说,Parchment是Blot的父级,很多个Blot组装成一个Parchment。

Blot分类:

ContainerBlot(容器节点)ScrollBlot root(文档的根节点,不可格式化)BlockBlot 块级(可格式化的父级节点)InlineBlot 内联(可格式化的父级节点)

ScrollBlot的实例数据构造:

{ "domNode": {}, // 真实的DOM节点 "prev": null, // 前一个元素 "next": null, // 后一个元素 "uiNode": null, "registry": { // 注册的信息 "attributes": {}, "classes": {}, "tags": {}, "types": {} }, "children": { // 子元素的节点描述,为一个链表 "head": null, // 第一个元素 "tail": null, // 末了一个元素 "length": 0 // 子元素长度 }, "observer": {} // DOM监听器}DOM变革与Parchment之间的数据同步

文档数据描述固然好,但是真实DOM和数据模型如何实现实时同步呢?

在ScrollBlot中,有个MutationObserver,去实时监测DOM变革。
当DOM发生变革时,会根据侦测到的真实DOM,去查找对应节点的blot信息,真实DOM与blot缓存在Registry中,以一个WeakMap的形式存储,详细缓存可见:

// parchment\src\registry.tspublic static blots = new WeakMap<Node, Blot>();

根据MutationObserver回调的变革信息,实行对应的blot update,以blockBlot为例,其update方法如下:

// public update( mutations: MutationRecord[], _context: { [key: string]: any },): void { // 调用ParentBlot中update方法,对新增和删除节点做逻辑同步 super.update(mutations, context); // 更新样式的逻辑同步 const attributeChanged = mutations.some( (mutation) => mutation.target === this.domNode && mutation.type === 'attributes', ); if (attributeChanged) { this.attributes.build(); }}Parchment映射成Delta的过程

有了Parchment对DOM的抽象,就方便对文档字符位置和样式进行扁平化的描述,以编辑器初始化为例,看看Quill是如何获取文档模型的Delta。

获取ScrollBlot中所有的Block,默认从Block开始处理,即最小颗粒度是块级元素

// editor.ts中获取delta方法getDelta(): Delta { return this.scroll.lines().reduce((delta, line) => { // 以Block为维度,分别获取每行的delta描述 return delta.concat(line.delta()); }, new Delta());}// scroll.ts中获取所有line的方法,即Blocklines(index = 0, length = Number.MAX_VALUE): (Block | BlockEmbed)[] { const getLines = ( blot: ParentBlot, blotIndex: number, blotLength: number, ) => { let lines = []; let lengthLeft = blotLength; blot.children.forEachAt( blotIndex, blotLength, (child, childIndex, childLength) => { // 最小颗粒度为Block if (isLine(child)) { lines.push(child); } else if (child instanceof ContainerBlot) { lines = lines.concat(getLines(child, childIndex, lengthLeft)); } lengthLeft -= childLength; }, ); return lines; }; return getLines(this, index, length); }获取每行数据的delta描述

// block.tsdelta(): Delta { if (this.cache.delta == null) { this.cache.delta = blockDelta(this); } return this.cache.delta;}function blockDelta(blot: BlockBlot, filter = true) { return ( blot // @ts-expect-error .descendants(LeafBlot) // 获取所有叶子节点 .reduce((delta, leaf: LeafBlot) => { if (leaf.length() === 0) { // 叶子节点的长度 return delta; } // 插入一个delta描述符,包含位置,样式描述 return delta.insert(leaf.value(), bubbleFormats(leaf, {}, filter)); }, new Delta()) .insert('\n', bubbleFormats(blot)) );}

获取delta的过程也是遍历至叶子节点,根据叶子节点的位置进行打算。

结语

以上只是对Quill的核心观点的大略描述,还有很多细节没有做过多的阐述,如如何注册自定义扩展、Quill的渲染流程、Parchment架构等,后续文章会逐步进行阐述。

标签:

相关文章

今日头条算法如何打造个化推荐系统

信息爆炸的时代已经到来。人们每天都要面对海量的信息,如何在海量信息中找到自己感兴趣的内容,成为了许多人关注的焦点。今日头条作为一款...

网站建设 2025-01-31 阅读0 评论0