引言
约六年多前,笔者初入前端,在大四时成为一名前端实习切图仔。彼时的前端,正在产生天翻地覆的变动:前端框架,React、Vue 正攻城略地,步步鲸吞 jQuery 的领土;前端工程化,Gulp/Grunt 还没站稳脚尖,又面对着 Webpack 滔天的洪水;大前端的概念衰亡,从不禁忌地表白其一统所有前端设施端的野心。新技术此起彼伏,令人目迷五色,又让人深感不安。每个新人,既心愿在大改革之际,乘风破浪,裹足不前,又胆怯卷入乱流,搁浅前滩。
过后,对前端布局的支流观点:”3 年内入门纯熟,而后抉择细分方向,深刻坚固,造成本人的护城河“。而过后有哪些细分方向?几经求索,在知乎大佬的指导下,总结为:内核、富文本、可视化、工程化(当然,现在细分畛域更是百花齐放)。可能大佬们也想不到,当年或者随便的几句话,便化成了笔者六年的时光。实习一年,毕业后两年,一心切图,而后机缘巧合下到金山从事新的 Web 富文本 / 排版软件开发,后至今日头条,围绕图文创作发展工作,在畛域内已三年无余。其实三年的时光,以十年为度量的细分业余畛域,显得微不足道,所以笔者在提笔时诚惶诚恐,惟恐表述有误,贻笑大方;但又念在本人仍保留一丝世一大的浩然之气,师承 WPS,背靠头条,两三口康师傅茉莉花茶下肚壮胆,心一横,便厚脸皮写下此篇“富文本”的介绍类文章,为各位同仁提供一丝教训或思路。
如有舛误,敬请斧正。
编辑器有哪些内容
从形象的角度而言,编辑器可分为四层,别离为「数据 」、「 排版 」、「 渲染 」、「 交互」。
数据
数据,大略分为两方面,其一,是指源 文件和内存数据结构的互相转化 ,次要的步骤有:读取、解析、结构化、以及对应的逆运算;其二,是指 产生编辑行为时,内存数据结构变动的过程,便是常见的增、删、改、查。
读取解析
虽源文件类型各异,但万变不离其宗,读取解析,根本就笼罩了编译原理的次要步骤。当然,常见的编辑器作为应用层,个别不波及更底层的硬件优化,往往失去语法树后,便可代表解析的完结。
比方,HTML 的解析步骤,根本也能和以上步骤所对应,此链接有更具体的实现细节:
内存数据结构及其变动
在接触的我的项目中,对数据的操作往往是通过类索引(index)的形式进行。内部视觉看,富文本如同 一维数组 ,每项内容代表一个带格局的节点。这种形式的益处: 十分直观。
// 操作对象:字节跳动
insertText(0, '今日头条,'); // 今日头条,字节跳动
setBold(1); // 字节跳动
setColor(2, 'blue'); // 字节跳动
但具体实现则不然。
出于性能思考,须要引入“区间”(range)概念,使数据更紧凑。
[{start: 0, end: 100, content: '字节.... 跳动', style: { /***/},
start: 101, end: 200, content: '今日.... 头条', style: {/***/}
}]
另一方面,会引入“偏移”(offset)概念,其起因是在批改数据时,更新某一个区间不会导致所有区间雪崩式变动。
// 比方每个区间的 start,可能指的是绝对前一区间的偏移量
[{startOffset: 0, endOffset: 100, content: '字节.... 跳动', style: { /***/},
startOffset: 0, endOffset: 100, content: '今日.... 头条', style: {/***/}
}]
为了兼顾增删改查,一种通用的办法是确定某种大小关系,以此构建红黑树(现实状况下,增删改查均为 O(log(n))
的均衡二叉树),再针对高频操作或瓶颈加以优化。这是在前司学到的技巧,在 VS Code 的 Text Buffer Reimplementation 优化中也有相似的概念。
排版
排版,可约等于 数据在空间的排列形式。
影响排版的因素有二,其一是元素的 本身属性 ,另一个则是利用的 排版规定。比照极其思考计算机根底的数据层,排版则更多和“业务”(比方不同的排版规范)相结合。
富文本编辑器中的「富」,广义指的是多格局的图文,狭义则指 所有能在前端设施展现的元素,能够是图片或文字,也能够超链接,视频,文件,乃至各种内联利用。
元素
前端最常打交道的便是图片和文字。但应留神,因为浏览器要思考跨平台性,文字或图片能利用的 Css 款式,往往曾经是形象或阉割后的属性。比方字体,脱离 Css,能够找到更进一步的形容:FreeType;而对于图片,不同图片也存在差别的(PNG、JPEG)。在个别场合,图片在 Web 端和其它端上体现不统一,便可能是某些被阉割的属性在起作用。
排版规定
排版规定极其简单。前端相熟的盒子模型、弹性布局便是不同的排版规定。不言而喻,雷同元素在不同排版规定下的体现是不同的。乏味的是,在字体外部,也有着各种繁琐的规定。比方:因为字距对(Kerning pairs)属性,VA 和 V、A 所占空间是不同的。文字内排版规定,也常和语种关联。比方波及到阿拉伯语时,字体会产生“形变”:ب س ب ب 和 بسبب是雷同的;而当 ltr(从左到右)和 rtl(从右到左)文字相结合时,又会波及 Bidi 双向算法。
遗憾的是,笔者未能深刻实现某个排版规定,仅在此列举一二。
渲染
「数据」通过「排版」解决后,将失去的具体元素地位信息提交到前端设施的过程,便是「渲染」。
在笔者接触的工作内容中,富文本中的“渲染”要比图形学中的“渲染”简略很多。在富文本的渲染中,会强调 调度 (scheduler)和 层级 (layer),也重视 可视区(Viewport)。这基本上和一些人机交互教训无关,在性能吃紧的场合,会优先渲染可视区范畴内重要层级的内容,而推延可视区外、或不重要的内容,也即是分块、分阶段渲染。
而优化渲染模块时,会感到某些技巧似曾相识。比方:计算 (非排版局部)和 提交 两者离开(类比 React 中的 render 和 commit 两个概念),有助于保留「空间换工夫」的可能,尽可能提前解决耗时操作,从而实现诸如离线渲染,双缓冲等策略,防止白屏工夫过长。
当然,也接触一些新鲜的概念,比方:“字体回退(Font Fallbacks)”。因为版权等限度,在波及某些商用字体的状况下,编辑器会有回退策略,从列表中返回类似的字体,相似于 Css 中的 font-family: sans-serif, '微软雅黑'
。Emoji
作为非凡的字体,因存在平台差别,也须要有对应的回退。
交互
交互层面,则是大多数前端工程师所相熟的畛域了。交互可分为两方面,一方面指 和编辑器的互动 ,另一方面则是等 通常意义的前端组件(工具栏,侧边栏等,和编辑器互动性较弱)。
一个残缺的编辑流程,通常由 交互 触发(比方输出、拖拽等),进而批改 数据 ,触发 排版 ,最初告诉 渲染器 进行渲染刷新。
对应 Web 前端,能够是大家相熟的步骤:JavaScript(调用 Api 变更数据)-> 触发排版(Style、Layout)-> 告诉渲染(Paint、Composite)。因为排版引擎是浏览器的重要组成部分,当然不会短少这些形象。
在此还有一个乏味的概念:点击测试(Hittest)。能够简略认为其为一个纯函数,入参屏幕坐标(x,y),返回后果为以后坐标的数据上下文(比方索引、节点、格局等)。通过点击测试,能无效建设交互和数据、排版的通道,从而实现各种自定义交互。(比方:模仿 Hover 成果:通过监听鼠标挪动,检索以后鼠标坐标是否和某个文本元素碰撞,如果碰撞,即认为产生 Hover 行为)。这种形式能冲破通常意义的前端限度,比方容许不受档次限度,也能够在其它载体(比方 Web 端的 Canvas)模仿相熟的交互行为(比方自定义光标和选区)。一个简略的点击测试前端实现:对有序的排版数据做二分查找,再程序遍历无序元素(比方浮动元素)。
当编辑器遇上 Web
「数据」+「排版」+「渲染」+「交互」,形成了一个 平台无关 的富文本编辑器实现。
但其复杂程度也往往让人望而生畏。所幸的是,Web 的蓬勃发展升高了实现富文本编辑器的难度,驾驭 HTML + CSS + JS 三剑客的浏览器,在极大水平上,屏蔽了数据、排版、渲染上的实现难度,使开发者能将更多的精力聚焦在「交互」细节上。
在 Web 富文本畛域,有一个绝对通用的等级:
- Lv0:
TextArea
+document.execCommand
- Lv1:
contenteditable
+observerable
+parser
- Lv2:自排(指排版)自绘(指渲染)
Lv0 级别的编辑器,在表现形式和兼容性上存在十分大的局限,更多是作为一些表单组件应用。
Lv1 级别的编辑器(少数开源的富文本都是这种实现形式),在监听 DOM 变动时,通过语法模型束缚,能较大水平实现编辑行为的绝对对立,但对一些非标准化的行为(比方光标、选区),在不同的浏览器上会存在各种轻微的差异或 Bug。
Lv0、Lv1 的痛点,在 Lv2 级别的编辑器中能失去无效的解决。但 Lv2 级别的编辑器,也存在比拟显著的缺点。其一,任何细小的性能,都可能须要思考数据、排版、渲染、交互各层级的实现,从而导致研发老本过高;其二,脱离前端根底,对轻量级前端集成能力较差,比方,很难在基于 OOXML(DOCX 的源格局)的排版软件上,实现类 Notion 的产品。
以上层级 只代表实现的难度 (或对细节的掌控力度),而不代表产品的好坏,毕竟绝大多数产品,是以需要而不是技术为导向。笔者曾有幸参加后两种编辑器的研发,因而有一个不成熟的观点:“在非专业商用场合,一个以兼顾用户体验、开发进度为目标 Web 富文本编辑器,Lv1 > Lv2”。理由很简略,Web 端的体现形式曾经足够灵便:其 排版形式能笼罩绝大多数罕用的场景 ;也足够优良:古代型浏览器对性能的压迫和各种奇淫巧技的使用曾经到了令人发指的境地(可通过 V8 博客窥探一二),断不是单线程的 Js、或缩水版多线程(指 WebWorker)所能比较的, 自排自绘的 Web 编辑器或多或少都会遇到性能瓶颈 ,在晦涩水平上逊色于基于 contenteditable
的浏览器。最重要的是,构建于 Html 之上的编辑器,学习老本绝对平顺, 研发和保护老本要低于 自排自绘的编辑器。因为,如果团队不具备编辑器相干的深厚研发能力,Lv1 级别的 Web 富文本编辑器是不二抉择。
Prosemirror
在此安利下 Prosemirror 框架,作者 Marijn Haverbeke,同时也是 CodeMirror、acorn、lezer 的作者,曾经在 Web 编辑器畛域沉迷了十多年,目前作为独立的软件开发者,在开源社区中十分沉闷。
Prosemirror 是一款优良的框架,实现了编辑器的根本个性,清晰的语法解析反对,齐备的事务零碎,甚至反对多人合作等。但与其它富文本组件不同,Prosemirror 定位更像是一个编辑器的 骨架 ,而不是一个齐备的编辑器。这意味着,开发者能够基于这个骨架,灵便地实现各形态各异的编辑器。既能够类 UEditor 这样的富文本,也能够是类 Notion 的合作工作台,或者是类秀米那样的图文排版工具(只是说晓得有基于此框架的同类型实现,并不指以上工具采纳此框架)。如果开发者 对编辑器的有强烈的定制化要求,Prosemirror 不逞多让。但在应用的过程中,Prosemirror 也有比拟显著的弊病。一方面该框架由外国大神编写,仅蕴含大量的英文示例和绝对艰涩的 API,同时编辑器也是绝对小众的畛域,从而导致中文文档匮乏;另一方面,Prosemirror 的布局更偏差于扩展性骨架,无奈即插即用(作者原话:The core library is not an easy drop-in component—we are prioritizing modularity and customizability over simplicity),导致采纳 Prosemirror 的学习和应用老本要比其它框架平缓,劝退了一部分开发者。
广告工夫
那么,是否有某种框架,既能最大限度保留 Prosemirror 的拓展性,又能提供各种插件,以即插即用呢?Syllepsis 理解一下。手动狗头。
Syllepsis 诞生于头条号,初衷是服务于业余作者创作的一个富文档编辑器,后延长到其它部门,其外部版 已被今日头条、西瓜视频、幸福里、懂车帝等超过 30+ 的部门打磨应用。能够简略认为,Syllepsis 是建设在 Prosemirror 根底上的 React 组件(已经也有 Vue 版本,但因为下层起因,现在只保留可拓展的接口),其目标只有两个:
- 提供常见的编辑插件,更简洁的接口,保障开箱可用,简略可配置。
- 保留 Prosemirror 易拓展的个性,在现有插件无奈满足需要时,仍保留定制的能力。
当然,该我的项目在开源方面还在起步阶段,你可通过文档进一步理解 Syllepsis 的能力。笔者认为,结构一个沉闷的生态才是开源我的项目能源远流长的外围因素,欢送所有同学在 Issue 上畅所欲言,反馈 Bug,提供对富文本编辑器的见解或需要。
结语
编辑器其实是一个非常复杂的模块,单凭集体难以实现所有的内容。笔者也只是在各前辈积攒的根底上,联合本身的工作内容,讲述本人的见解。限于篇幅和能力,很多中央只是一笔带过,欢送同行勘察和补充。最初,感激 WPS 的烈锦同学,阿里的小海豹同学提供写作灵感和批改倡议。
参考
- 浏览器工作原理