Quill编辑器实现原理初探
从事前端开发的同学,对富文本编辑器都不是很陌生。但是大多数富文本编辑器都是开箱即用,很少会对其实现原理进行深入的探讨。假如静下心去细细品味,会发现想要做好一款富文本编辑器,需要对整个前端生态有较深入的理解。在某种意义上说,富文本编辑器是前端一个集大成者。
富文本编辑器根据其实现方式,业内将其划分为L0 ~ L2 ,层层递进,功能的支撑也越来越强大。
阶段
描述
典型产品
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(n*h) ,这无疑是一种巨大的性能消耗,因此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 的形式存储,具体缓存可见:// parchmentsrcregistry.ts public static blots = new WeakMap();
根据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的方法,即Block lines(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.ts delta(): 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(" ", bubbleFormats(blot)) ); }
获取delta 的过程也是遍历至叶子节点,根据叶子节点的位置进行计算。结语
以上只是对Quill 的核心概念的简单描述,还有很多细节没有做过多的阐述,如如何注册自定义扩展、Quill 的渲染流程、Parchment 架构等,后续文章会慢慢进行阐述。
撒盐哥撒到伤口上2022卡塔尔世界杯完美收官,梅西圆梦卡塔尔。可是赛后出现一个跟不和谐的画面,一个网红竟然用他的臭嘴亲吻了大力神杯他就是撒盐哥阿根廷拿到世界杯和他半毛钱关系都没有,又是强拉梅西拍照
默许违占耕地顶风批地占地耕地保护不力背后的问题近日,自然资源部公开通报2021年耕地保护督察发现的45个土地违法违规典型案例,涉及河北安徽山东云南广西等省区和部分国有企业。通报案例主要包括侵占耕地挖湖造景违法占用耕地绿化造林以
完美世界石昊的功法与宝术荒天帝石昊能纵横九天十地离不开其强大的宝术与功法,今天我们就来说说石昊掌握的功法与宝术。喜欢的小伙伴留下关注吧!以身为种所有生灵都能修习的人体秘境修炼体系,它与融合道种踏仙路是完全
心乱思静,给心充个电近期,谁的内心没点焦虑不安呢?够了,生命如此短暂,短得如此华美,不值得你去纠结怀疑恐惧浪费它!赛涅卡说过我们何必为人生的片段而哭泣,我们整个生命都催人泪下。要去见山海,去会众生,去
为什么女人很难放下前任图片来自网络,图文无关有时候,即使两个人分开了,但是女人还是保留对方的联系方式,保留着过去的相册照片,或者聊天记录。没准女人的账号密码,还是前任的生日,诸如此类的。为什么,分开后,
墨菲定律罗杰斯论断未雨绸缪,主宰命运我是未城朝雨,用文字表达我对生活的态度。未雨绸缪。有备无患对待问题的态度应该像对待疾病的态度一样,在身体有些不适的时候,就要及时治疗以免病情发展得更为严重,甚至无法医治,对问题也是
一分钱不花,如何变得更加幸运?一守良知。想一切事,做一切事,遵守作为人的良知。比如,不要看到个m或r国人遭殃了,你就开心。大家都是中国人,有民族情节,我也有,但是作为中国人的前提是,我们是一个好人,守良知有正义
阿根廷获得5200万奖金,队员能有多少呢?海参队估计是看不上吧卡塔尔世界杯总奖金是4。4亿美金,前四的球队奖金分别是冠军阿根廷4200万美元亚军法国3000万美元季军克罗地亚2700万美元殿军摩洛哥2500万美元其中冠军阿根廷外加南美足协另外
饶毅对钟南山张文宏的指责,可能源于核酸检测造假饶毅校长,我们和解吧饶毅校长提出,国家在疫情防控政策的调整上,存在操之过急的情况,他认为,国家在这个事情上,没有实事求是,没有循序渐进。也就是说,饶毅校长反对国家放宽疫情防控的限制
收藏!望牛墩医院全面恢复诊疗,全新医疗服务指南来了12月20日,望牛墩医院发布相关公告,从12月21日(星期三)8时起全面恢复门诊急诊发热门诊体检科住院诊疗等面向社会的医疗服务,望牛墩医院核酸检测点恢复对外24小时服务,具体通告内
阳过以后,如何开启运动?看专家怎么说在国内防疫政策做了巨大调整之后,国内各地出现了大量的奥密克戎感染病例,一时间诞生了许多小阳人,不过基本在一周时间之内,第一批大规模感染奥密克戎的人群就恢复了正常生活。但是有人指出,
冯巩和冯国璋的关系(多图)看上面这张图片,左边是冯国璋,右边是冯巩在建军大业饰演的冯国璋,看着是不是高度相似,这就对了,冯国璋的确是冯巩的曾祖父。冯国璋冯国璋从历史的长远角度来看,冯巩祖上的家族还是十分辉煌
中国云南元谋发现最早的长臂猿化石近日,中国科学院昆明动物研究所等科研团队对在中国西南(云南元谋)新发现的小型猿类化石进行研究,获得了已知最早长臂猿的证据。云南元谋新发现的被命名为元谋小猿的小型猿类,被科学家团队确
云南元谋发现最早的长臂猿化石中新社昆明9月27日电(记者胡远航)记者27日从中国科学院昆明动物研究所获悉,该所科研人员在云南省楚雄彝族自治州元谋县新发现的元谋小猿,被确定为迄今发现的最早的长臂猿祖先化石。这一
让Windows兼容Android,英特尔十年前就开始准备了一直以来如何像macOS与iOS那般,流畅的打通Windows和Android系统是许多人关注的问题。前一段时间,Windows11正式宣布通过亚马逊应用商店的形式开始原生支持An
WebDAV之葫芦儿派盘FlacboxFlacboxHIRES音乐播放器音频均衡器,聆听您自己的高品质音乐支持webdav方式连接葫芦儿派盘。Flacbox是一款带有音频均衡器和低音增强器的高分辨率音乐播放器。使用此应
远古发现丨发现已知最早的长臂猿化石!原来是它古生物学家在云南元谋盆地发现了距今700万年至800万年的小型猿类化石,命名为元谋小猿,并证明这是已知最早的长臂猿。该成果已于近日发表在国际期刊人类进化杂志上。长臂猿科现存20个种
我国西南发现已知最早的长臂猿化石新华社昆明9月27日电(记者岳冉冉)古生物学家在云南元谋盆地发现了距今700万年至800万年的小型猿类化石,命名为元谋小猿,并证明这是已知最早的长臂猿。该成果已于近日发表在国际期刊
山东大学成功研制高质量4英寸氧化镓晶体近日,山东大学陶绪堂教授团队使用导模法(EFG)成功制备了外形完整的4英寸(001)主面氧化镓(Ga2O3)单晶,并对其性能进行了分析。劳厄测试衍射斑点清晰对称,说明晶体具有良好的
科学家发现雄绵羊长寿秘诀,或适用于人类,却令人无比纠结综述长寿和永生一直是人类所追寻的一个目标,然而生老病死作为自然界中亘古不变的定律,几乎不可能被人为改变。不过我们也一直没有放弃追寻延长寿命的方法。随着医疗条件的进步,人类的平均寿命
成都又一后花园城市整洁,美味无限,自驾2h直达小伙伴们,大家好,我是菜菜,咱们继续聊旅游吧。遂宁这个城市并不大,也没有成都那样的繁华与喧嚣,但是它整洁休闲,好吃好玩还有特有的文化底蕴,它淡然出世,与繁华保持着恰到好处的距离。正
旭辉非标债务违约,暴雷了?据某位接近旭辉的人士透露,有一笔某地产集团的私募债即将在月底到期,但是某地产集团的想法是暂不兑付,给投资人谈展期(违约)除了这个投向北京的私募债,投资人还投了某地产集团杭州和惠州的