深入浅出富文本编辑器

article/2025/8/22 19:10:08

‍大厂技术  坚持周更  精选好文

编辑器介绍

常见的富文本编辑器现实方式可以分成两大类,分别是用 textarea 和 contenteditable 来实现。

textarea

结构简单使用方便,一些文本格式和复杂的样式难以实现,推荐仅在对编辑要求不高的场景使用。

contenteditable

将元素的 contenteditable 属性设为 true时,该元素则成为了编辑器的主体。配合 document.execCommand 能够实现绝大多数功能,主流编辑器是基于 contenteditable 来设计的。

但是单纯依赖 contenteditable 直接产出 html 会带来一些问题,例如相同的输入在不同浏览器下的输出可能不一致,相同的输出在不同浏览器中展示存在差异,并且这些问题在移动端会被放大,同时 html 使用具有局限性,不方便在跨平台间使用。

因此更好的方案是制定一套数据结构 + 文档模型,所有的输入都经过编辑器生成约定的产物,这样在不同的平台均可解析并且保证得到预期的效果。

还有一类是以 Google docs 为主的编辑器,不使用 contenteditable ,而是基于 canvas 渲染[1],通过监听用户输入,模拟编辑器的运行,此类编辑器实现成本极高且复杂。

本文以 quill[2] 为例,介绍如何实现一个支持跨平台渲染,且可以插入自定义模块的富文本编辑器。

基本概念

delta[3]

用于描述富文本内容或内容变换的数据结构,纯 json 格式,能够转化成 js 对象后方便操作,基本格式如下,由一组 op 组成。

op 是个 js 对象,可理解为对当前内容的一次变更,它主要有以下几个属性。

insert: 插入,后面 【3.2 数据结构】有介绍可能的值和对应的含义

retain: 值为 number 类型,保留相应长度的内容

delete: 值为 number 类型,删除相应长度的内容

上面三个属性必有且仅有一个出现在 op 对象中

attributes: 可选,值为对象,可描述格式化信息

如何理解内容或内容变换,举个🌰,下面这段数据表示了内容 “Grass the Green”,

{ops: [{ insert: 'Grass', attributes: { bold: true } },{ insert: ' the ' },{ insert: 'Green', attributes: { color: '#00ff00' } }]
}

经过下面一次 delta 内容变换后新内容为 “Grass the blue”。

{ops: [// 接下来 5 个字符取消加粗并加上斜体格式{ retain: 5, attributes: { bold: null, italic: true } },// 维持 5 个字符不变{ retain: 5 },// 插入{ insert: "Blue", attributes: { color: '#0000ff' },// 删除后面 5 个字符{ delete: 5 }]
}

Delta 本质上是一系列操作记录,在渲染时可以看作记录了从空白到目标文档的一个过程,而 HTML 是一个树形结构,所以 Delta 的线性结构相比 HTML 在业务使用上有天生优势

parchment[4]

一种文档模型,由 blots 组成,用来描述数据,可以拓展自定义的数据。

<p>一段文字加视频的富文本内容。<img src="xxx" alt=""></p><p><strong>加粗文本结尾。</strong>
</p>

parchment 与 blot 关系类似于 DOM 与 element node,上面一段 html 内容使用 dom tree 和 parchment tree 描述分别如下图所示。

88237ea9b184ce8f2c015d1e06dba70d.png

parchment 提供了几种基础 blot,同时支持开发中根据需求拓展定义自己的 blot,后面会演示如何开发一个自定义的 blot。

{// 基础节点ShadowBlot,// 容器节点 => 基础节点ContainerBlot,// 格式化节点 => 容器节点FormatBlot,// 叶子节点LeafBlot,// 编辑器根节点 => 容器节点ScrollBlot,// 块级节点 => 格式化节点 BlockBlot,// 内联节点 => 格式化节点 InlineBlot,// 文本节点 => 叶子节点TextBlot,// 嵌入式节点 => 叶子节点EmbedBlot,
}

最后用一张图了解下 quill 内部的工作流程,其中开发者需要关注的业务层逻辑十分简洁,可以通过手动输入和 api 方式变更编辑器内容,同时 editor-change 事件会输出当次操作和最新内容对应的 delta 数据。

b513859f5b063d4fe2e6f225e9b739b5.png

实际应用

数据流

在业务中,基本数据流应该如下图所示,由编辑器生成 delta 数据,之后由相应平台的解析器渲染成对应的内容。

c90094fa961967305b95dda11c52184a.png

数据结构

良好的内容数据结构设计,在后续维护和跨平台渲染时起到关键作用,我们可以将富文本内容中依赖的媒体(图片、视频、自定义的格式)数据放到外层来,通过 id 关联,这样日后拓展和渲染时会比较方便。

interface ItemContent {// 富文本数据,存储着 delta-stringtext?: string;// 视频videoList?: Video[];// 图片imageList?: Image[];// 自定义的模块,如投票、广告卡片、问卷卡片等等customList?: Custom[];
}

其中编辑器输出的是标准 delta 数据, 结构如下所示,

// 纯文本, \n 代表换行
{insert: string;
},// 特殊类型的文本
{insert: '超链接文本',attributes: {// 文字颜色color: string,// 加粗bold:  boolean,// 超链接地址link: string;...,}
},
// 有序无序列表
{insert:  '\n',attributes: {list: 'ordered' | 'bullet'}},
{insert: {uploading: {// 资源类型type: 'image' | 'video' | 'vote' | 'and more...'// 资源 iduid: string},},
},
// 图片
{insert: { image: '${image_uri}' }
},
// 视频
{insert: {videoPoster: {/** 视频封面地址 */            url: string;/** 视频 id */            videoId: string;}}
},
// 投票
{insert: {vote: {voteId: string}}
},
// 缩进,作用域内所有文本向右缩进 indent 个单位;
// 作用域:从当前为起始位置向前回溯,遇到以下任意一种情况结束
// 1、纯文本 \n
// 2、attributes的属性含有indent并且indent值小于等于当前值
{insert:  '\n',attributes: {indent: 1-8,}
},

图片 / 视频混排

图片上传需要支持展示上传中的状态,并且不应该阻塞用户的编辑,所以需要先使用一个占位元素,待上传完成后将占位替换成真实图片或视频。

自定义 blot

自定义 blot 的好处是能够将整个的功能(例如图表功能)封装到一个 blot 中,这样业务开发时可直接使用,而不用管每个功能是怎么实现的。下面以图片视频上传态占位 blot 为例,演示如何自定义一个 blot。

import Quill from 'quill';enum MediaType {Image = 'image',Video = 'video',
}interface UploadingType {type: MediaType;// 唯一的 id,当图片或视频上传完成后,需要找到对应的 uid 进行替换uid: string;
}export const BlockEmbed = Quill.import('blots/block/embed');class Uploading extends BlockEmbed {static _value: Record<string, UploadingType> = {};static create(value: UploadingType) {const ELEMENT_SIZE = 60;// blot 对应的 dom 节点const node = super.create();this._value[value.uid] = value;node.contentEditable = false;node.style.width = `${ELEMENT_SIZE}px`;node.style.height = `${ELEMENT_SIZE}px`;node.style.backgroundImage = `url(占位图地址)`;node.style.backgroundSize = 'cover';node.style.margin = '0 auto';// 用来区分对应资源node.setAttribute('data-uid', value.uid);return node;}static value(v) {return this._value[v.dataset?.uid];}
}Uploading.blotName = 'uploading';
Uploading.tagName = 'div';export default Uploading;

将自定义 blot 注册到编辑器实例中,使用 quill 的 insertEmbed 来调用这个blot 即可。

// editor.tsx
Quill.register(VideoPosterBlot);quill.insertEmbed(1, 'uploading', {type: 'image',uid: 'xxx',
});
7d9f7c04de8e6491040aa207d2ec1c62.png

处理粘贴操作

复制粘贴可以大幅提升编辑器效率,但是我们需要对剪切板中的视频和图片进行特殊处理,将剪切板中的内容转化成自定义的格式,并自动上传其中图片和视频。

基本原理

监听用户的粘贴操作,读取 paste event[5] 返回的 clipboardData[6] 数据,二次加工后再插入编辑器中。

target.addEventListener('paste', (event) =>  {const clipboardData = (event.clipboardData || window.clipboardData)const text = clipboardData.getData('text',);const html = clipboardData.getData('text/html',);/*** 业务逻辑*/event.preventDefault();
});

clipboardData.items 是 DataTransferItem 的数组集合,它包含了本次粘贴操作的数据内容。

DataTransferItem 有两个属性分别是 kindtype,其中 kind 值通常是 string 类型,如果是文件类型的数据那么值为 filetype 值是 MIME 类型,常见的是 text/plain 和 text/html

处理图片

剪切板中的图片来源分为两大类,一是直接从文件系统中复制,这种情况我们

从文件系统中复制

5f5e0573f7ae36f6a864036b36f37691.png

从文件系统中复制粘贴后,能获取到 File 对象,那么直接插入编辑器中,即可复用前面的图片上传逻辑。

从网页复制

dda695e0d5f5811ce807eb5e27760326.png44cf2fe9a7c0473efd1806a5e42ca985.png

从上面右图不难看出,从网页中复制过来的内容中包含 text/html 富文本类型,由于图片可能是临时地址,直接使用三方图片地址不可靠,需要把 html 中图片地址提取出来,下载后再上传至我们自己的服务器中,图片上传模块还能继续复用上文的图片混排。

fffdb2366eb7c227219fd9acd6742973.png

上文内容的 dom 树基础结构如图所示,可以经过后序遍历将所有节点处理成数组结构,当遇到节点为图片时则调用上面的图片混排逻辑。

convert({ html, text }, formats = {}) {if (!html) {return new Delta().insert(text || '');}// 返回 HTMLDocument 对象const doc = new DOMParser().parseFromString(html, 'text/html');const container = doc.body;// key - node// value - matcher: (node, delta, scroll) => newDeltaconst nodeMatches = new WeakMap();// 返回两个匹配器,分别处理 ELEMENT_NODE 和 TEXT_NODE ,将 dom 转化成 Deltaconst [elementMatchers, textMatchers] = this.prepareMatching(container,nodeMatches,);return traverse(this.quill.scroll,container,elementMatchers,textMatchers,nodeMatches,);
}function traverse(scroll, node, elementMatchers, textMatchers, nodeMatches) {// 节点为叶子节点即文本if (node.nodeType === node.TEXT_NODE) {return textMatchers.reduce((delta, matcher) =>  {return matcher(node, delta, scroll);}, new Delta());}if (node.nodeType === node.ELEMENT_NODE) {return Array.from(node.childNodes || []).reduce((delta, childNode) =>  {let childrenDelta = traverse(scroll,childNode,elementMatchers,textMatchers,nodeMatches,);if (childNode.nodeType === node.ELEMENT_NODE) {childrenDelta = elementMatchers.reduce((reducedDelta, matcher) =>  {return matcher(childNode, reducedDelta, scroll);}, childrenDelta);childrenDelta = (nodeMatches.get(childNode) || []).reduce((reducedDelta, matcher) =>  {return matcher(childNode, reducedDelta, scroll);},childrenDelta,);}return delta.concat(childrenDelta);}, new Delta());}return new Delta();
}

上面例子中的数据可以转化成以下 delta 数据,视频的处理方法与图片类似,这里不再赘述。

{ops: [{insert: '说起艾冬梅这个名字,现在的年轻人可能不是很熟悉,但是她曾经却是家喻户晓的人物,'},{insert: '艾冬梅是我国著名的马拉松运动员'  ,         attribute: {bold: true},},{insert: '。她出生于1981年,是个来自东北的姑娘,和很多普通的八零后一样,她来自一个平凡的家庭,从小生活十分幸福,家境虽然不富裕,但艾冬梅依然是父母的掌上明珠。'},{insert: {image: {url: 'xxx'}}},{insert: '但是艾冬梅和其他人不同的是她从小就展现出了惊人的长跑天赋'  ,         attribute: {bold: true},},{insert: ' , 1993年当时艾冬梅还在念小学,她在一次跑步比赛中获得了一个十分优秀的成绩,在脚趾头受伤的情况下打破了当地的3000米项目记录,远远超过了参赛的所有人。这让很多人都十分震惊,于是艾冬梅顺利地被齐齐哈尔体校选中。'}]
}

解析数据

在 web 场景下可以使用 quill-delta-to-html[7] 这个库来做解析,如果是小程序,对于媒体元素(如:小程序中图片必须要指定宽高[8])支持相对不太友好,需要自己解析,下面简单介绍下如何渲染 delta 数据。

由于 delta 是一个线性结构,转化成 dom 时,需要构建一棵树,将块级元素的子元素关联到它的 children 中。

8e0a5658dc72cd9873f4a7abd26c4fd9.png

上图中的原数据经过第一轮处理

  1. 纯文本反规范化,将 abc\ndef\ng 格式转化成 [abc, \n, def, \n, g]

  2. 将块级元素的元信息,写入第一个 op 中

块级元素的元信息包括:缩进,有序列表序号,【当前元素所在块级元素】在原数据中的起始与终止索引,【当前元素所在块级元素】在 dom 列表中的索引

62c65ca07e306182f6a8a00a70b7d334.png

经过上面转化后原数据变成上图中的格式,每个 op 都含有相应的元数据,接下要做的就是解析这些 op,将其转化成 Element。

009c0b35065bb2c92a9d711235f7296d.png

对于自定义 blot 的渲染,我们可以封装成组件(react 或 vue 组件,取决你使用什么框架),这样业务功能和编辑器开发可解耦,不了解编辑器代码的同学也能够参与开发。

小结

至此,我们已经了解开发编辑器的基本流程和需要重点关注的一些事项。如果业务中需要拓展一些功能卡片,如飞书文档的各种应用,可通过拓展 blot + 编写对应的组件来实现。此外还能够通过编写相应平台的解析器在非 web 场景的展示,轻松实现内容跨平台渲染。

❤️ 谢谢支持

字节跳动校/社招投递链接: https://jobs.toutiao.com/s/2aHmuX4

内推码:EQHHRR5

参考资料

[1]

基于 canvas 渲染: https://workspaceupdates.googleblog.com/2021/05/Google-Docs-Canvas-Based-Rendering-Update.html

[2]

quill: https://quilljs.com/docs/quickstart/

[3]

delta: https://quilljs.com/docs/delta/

[4]

parchment: https://github.com/quilljs/parchment

[5]

paste event: https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event

[6]

clipboardData: https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent/clipboardData

[7]

quill-delta-to-html: https://www.npmjs.com/package/quill-delta-to-html

[8]

指定宽高: https://developers.weixin.qq.com/miniprogram/dev/component/image.html

- END -


http://chatgpt.dhexx.cn/article/04UEMMA0.shtml

相关文章

推荐几个非常不错的富文本编辑器

1、wangEditor——基于javascript和css开发的 Web富文本编辑器&#xff0c; 轻量、简洁、界面美观、易用、开源免费。 界面截图&#xff1a;官网地址 2、TinyMCE——TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器&#xff0c;由JavaScript写成。它对IE6和Firefox1.5都有…

vue使用富文本编辑器vue-quill-editor

问题描述&#xff1a; 我们在开发过程中经常会遇到使用富文本编辑器进行操作&#xff0c;当前插件市场上关于富文本的编辑器挺多的&#xff0c;我们就不一一个介绍了&#xff0c;现在我们主要讲解一些vue-quill-editor富文本编辑器的使用操作和注意事项。 效果图 具体操作使用…

LayUI - 富文本编辑器

一个做后端的猿&#xff0c;难免用到LayUI&#xff0c;首先在这里&#xff0c;不推荐使用&#xff0c;坑多 我这里用的是layui-v2.5.7版本 一、富文本编辑器 缺点&#xff1a;功能太少&#xff0c;只能满足简单需求&#xff0c;只有下面这几个功能 废话少说&#xff0c;直接丢…

最佳文本编辑器

原文&#xff1a; donationcoder.com  译者&#xff1a; xbeta善用佳软  说明&#xff1a;仅做翻译&#xff0c;忠实原文。不代表同意文中观点&#xff08;xbeta认为最好的编辑器为VIM&#xff09;。 最佳文本编辑器 当前&#xff0c;好用的文本编辑器比比皆是——无论商…

Qt实现文本编辑器(一)

在Qt中QMainWindow是一个为用户提供主窗口程序的类&#xff0c;包含了&#xff1a;菜单栏、工具栏、锚接部件、状态栏以及一个中部件。今天我就来通过实现一个简单的文本编辑器讲解下对QMainWindow的各种功能讲解。 想要完整的实现一个编辑器&#xff0c;所需要的功能还是比较…

文本编辑器推荐

对于程序员或者开发者来说&#xff0c;可以通过电脑文本编辑器来完成语言的编译或输入&#xff0c;那么文本编辑器哪个好呢&#xff0c;其实使用系统自带的文本框就不错&#xff0c;当然还有不少其他软件可用。 文本编辑器哪个好&#xff1a; 一、记事本 1、这是系统自带的文…

富文本编辑器汇总

富文本编辑器&#xff1a;&#xff08;Rich Text Editor&#xff0c;RTE&#xff09;是一种可内嵌于浏览器&#xff0c;所见即所得的文本编辑器。它提供类似于Office Word 的编辑功能&#xff0c;方便那些不太懂HTML用户使用&#xff0c;富文本编辑器的应用非常广泛&#xff0c…

十五种文本编辑器

很多时候比如编程查看代码或者打开各种文档下我们都会用到文本编辑器&#xff0c;Windows自带的记事本功能很简陋并且打开大文件很慢&#xff0c;因此很多童鞋都会有自己喜欢的一款文本编辑器。在这里&#xff0c;西西挑选前15个最佳的文本编辑器&#xff0c;这些编辑器实际上主…

强烈呼吁弃用Notepad++,优秀替代品献上

Notepad作为一款开源文本编辑软件&#xff0c;无可厚非是一款优秀的软件, 但是由于“种种原因”, 坚决弃用。 禁用NotePad 推荐三款优秀的免费替代品&#xff1a; 可以直接去官网下载&#xff0c;也可以在下面的网盘地址下载&#xff08;分别提供了安装版&#xff08;分32位和…

adreno性能天梯图_深度学习之GPU显卡性能天梯图

在深度学习的显卡市场&#xff0c;英伟达的地位还是暂时无人能够撼动的。专业卡暂不纳入考虑&#xff0c;毕竟性价比太低了。大家平时使用的还是老黄的游戏卡&#xff0c;性能排第一的就是Titan RTX了&#xff0c;具备24G大显存&#xff0c;然而售价也高达两万块。接下来就是大…

2013台式计算机,显卡天梯图 2013最新台式机显卡天梯图

在聊完笔记本显卡的性能后,小编当然要和大家聊聊重头戏了:台式机的显卡天梯图。台式电脑一直是玩大型3D游戏的必选机型,虽然没有笔记本的便携性,但是台式机可以让电脑显卡性能得到充分的发挥,主要是独立显卡的散热不像笔记本那样存在空间狭小的问题。 台式机显卡性能一直…

mx350显卡天梯图_显卡天梯图2020年终整理发布

购买显卡如何选择&#xff1f;当然是有参考才能更好的选择&#xff0c;可是我们要拿什么参考呢&#xff1f;没错&#xff0c;通过笔者一年的时间收集整理&#xff0c;我们将为大家带来本年度英伟达显卡和AMD显卡天梯图。 AMD显卡天梯图&#xff1a; 英伟达显卡天梯图&#xff1…

2020电脑服务器cpu性能天梯图,CPU性能天梯图[202002版]

CPU与显卡还不太一样&#xff0c;CPU有线程&#xff0c;线程多&#xff0c;那性能会强。以前我们对CPU的认识有一个误区&#xff0c;老以为i5就比i3强&#xff0c;或者i7就比i5强&#xff0c;其实不一定。您以后就别再听电脑城那些人乱说&#xff0c;“我给你配置的是i7 的CPU&…

服务器单核性能天梯图,台式机cpu性能排行(cpu单核性能天梯图)

【科技犬】 鲁大师发布 2021 年 Q1 季度电脑处理器性能排行榜&#xff0c;霸榜两年的 AMD Ryzen Threadripper 3990X 依然是第一名。最新发布的 Threadripper PRO 3995WX 数据太少达不到上榜要求。 如上图所示&#xff0c;鲁大师台式机处理器排行榜前四名都是 AMD 处理器&#…

mx350显卡天梯图_五月显卡性能排行 台式显卡天梯图2020年5月最新版

显卡天梯图(更新至2020年5月9日)主要用来纵向对比一下显卡性能的高低&#xff0c;这样对于我们在DIY装机过程当中&#xff0c;选购桌面显卡有个参考标准。台式桌面级显卡天梯图方便我们更直观地对比不同厂商最新显卡之间的性能高低。以下IT数码通为大家带来2020年5月最新显卡天…

显卡天梯图:2014最新显卡性能天梯图

随着电脑游戏的推广&#xff0c;很多用户都喜欢上了电脑网络游戏&#xff0c;所以组装电脑用户在装机的时候&#xff0c;会考虑电脑配置的游戏性能&#xff0c;要提高电脑配置游戏性能首要条件就是显卡性能要强&#xff0c;如果显卡性能不佳&#xff0c;那么其它方面性能再强&a…

CPU-显卡-硬盘性能天梯图排行榜源码

介绍&#xff1a; CPU天梯图和桌面显卡性能天梯图排行榜&#xff0c;硬盘性能天梯图排行榜源码分享&#xff01; CPU&#xff0c;GPU显卡&#xff0c;硬盘所有文件以进行本地化,嗨次元在线工具箱&#xff0c;CPU性能天梯表,CPU性能天梯表,CPU性能天梯图,GPU性能天梯图,显卡性…

桌面显卡天梯图2023年2月 台式机显卡天梯图2023

1、NVIDIA GeForce RTX 3090 Ti 24GB。 2、NVIDIA GeForce RTX 3090 24GB。 3、NVIDIA GeForce RTX 3080 Ti 12GB。 4、AMD Radeon RX 6900XT 16GB。 5、NVIDIA GeForce RTX 3080 12GB。 6、AMD Radeon RX 6800XT 16GB。 组装电脑怎么搭配显卡更合适这些点很重要看过你就懂了 h…

2014显卡性能天梯图,组装电脑备用

2019独角兽企业重金招聘Python工程师标准>>> 2014显卡性能天梯图&#xff0c;组装电脑备用&#xff0c;原来i7的HD4600是不如08年ATI的HD3850的(当时499元)&#xff1a; 转载于:https://my.oschina.net/lizhiling/blog/375346

英伟达显卡排名天梯图2022

英伟达显卡排名天梯图 英伟达显卡排名天梯图 英伟达显卡排名天梯图 3090ti 3070ti 3050ti 1、Fermi费米架构 费米是诺贝尔物理学奖得主&#xff0c;被称为原子能之父&#xff0c;他的实验小组建立了人类第一台可控核反应堆e69da5e6ba90323131333532363134313032313635333…