wangEditor - 开发web富文本编辑器的坑有哪些

6,225 阅读9分钟

序言

先说说为什么要写这篇文章吧,web富文本编辑器其实是一个天坑,浏览器支持的极差,用户的需求又高...。加入团队后在富文本编辑器领域深挖之后才发现,ε=(´ο`*)))唉,都是泪(不过好像每个领域深挖下去都是大坑)。今天就来以wangEditor为例讲讲我们产品的问题是什么,以及这几天调研出来的一些解决方案。

存在的问题

  1. 核心代码没有与功能代码区分开,我们现在不管是处理issue还是新增功能,都是直接在项目中修改。长此以往下去,我们项目最终会演变成一个巨石项目(这也是做插件化的初衷)。
  2. 插件与菜单存在强耦合关系,如果用户不需要菜单,把功能去掉后(editor.config.menus = []),功能也会失效。(实际案例:issue#3117)
  3. execCommand是富文本编辑器功能的核心,但其存在很多BUG,而且不同浏览器兼容性不一样,官方也放弃了它,不再维护。这说明该api是不可信任的。

image.png

相应的解决方式

插件化

在上一篇文章中我有提到过用核心core + plugin来代替核心core + plugin + menu的想法,不过没有讲原因,今天把我这样设计的原因讲一下。

想象这样一种场景,用户不需要你的菜单栏,只想要编辑区域和插入图片的功能。如果把源码设计为核心core + plugin + menu,那menu就纯粹是打酱油的,没有半点作用,甚至会加大项目的体积。

所以用核心core + plugin来代替它,即核心core + anything = everything的一个思路。举几个例子大家就能理解了:

  • 核心core + 菜单栏插件 = 富文本编辑器
  • 核心core + markdown插件 = markdown编辑器
  • 核心core + 菜单栏插件 + markdown插件 = 富文本 + markdown 的编辑器
  • 核心core + highlight.js + monaco-editor = 在线代码编辑器
  • ............

这样的话,wangEditor可能就不再仅仅局限于富文本这个领域,只要我们内核做的好,扩展性足够强,那么它将可以胜任所有编辑器场景!(小小的幻想一下哈哈,芜湖起飞!)

解耦菜单与菜单的关系

当前在wangEditor中,为编辑器赋予功能的方式是这样的:

image.png

当用户不想要Image菜单,把editor.config.menus项中的Image去除掉后,上传图片功能也随之被取消掉,如果用户想要上传图片功能,在当前就必须带上Image菜单项。(上传图片功能的一切功能代码都放到了Image构造器中)

针对这种情况,我们可以进行解耦,把上传图片这个具体的功能从Image菜单中解耦出来。这样上传图片功能就成为一个独立体,同时Image菜单可以依赖上传图片功能:

image.png

用户如果不需要菜单项,也可以直接注册插件功能:

image.png

图画的着实丑,各位大佬凑合着看^_^

execCommand

先带大家了解一下execCommand,MDN定义如下:

当一个HTML文档切换到设计模式(就是可编辑的意思,你可以通过设置dom的contenteditable为true来让一个dom可编辑)时,document暴露 execCommand 方法,该方法允许运行命令来操纵可编辑内容区域的元素。 大多数命令影响document的 selection(粗体,斜体等),当其他命令插入新元素(添加链接)或影响整行(缩进)。当使用contentEditable时,调用 execCommand() 将影响当前活动的可编辑元素。

execCommand就是通过接收一个命令,来影响当前选区(selection)的内容。你可以传入不同指令来产生不同的效果。

小科普

这里给大家做个概念的科普,要不然后面的名词可能会看不懂。

在富文本编辑器领域中,有三个等级:

  • L0: 借助原生contenteditable和execCommand实现。
  • L1: 借助原生contenteditable,自己实现execCommand
  • L2: 自己实现contenteditable和execCommand

wangEditor当前是L0,现在想升级为L1.

用MVC架构来看待wangEditor

MVC分别为Model,View,Control。在wangEditor中,他们分别是什么呢?

  • Model:编辑区域中的html代码
  • View:页面上编辑区域中展示的内容
  • Control:用户进行的操作(如加粗,斜体,撤销,恢复等) 可以发现,execCommand属于Control的一部分。它通过修改Model,让View做出相应的变化。

聊聊execCommand api的替代方案

社区中有很多execCommand的替代方案,比如自己自定义一些命令,搭配Selection的信息对DOM进行操作。但我感觉不太行,所以pass掉了(我感觉频繁的DOM操作会导致性能不好)

讲讲我觉得可行的一种方案:Model层用文档协议来代替html代码。相比之前直接修改html代码,这种性能要好很多,很多知名编辑器也是这么做的。

文档协议

那么什么是文档协议呢?作为开发人员,你肯定听过很多协议(http协议,tcp协议等)。这里的文档协议其实就是用一种数据结构来描述编辑区的内容。你可以把它类比为React中的fiber Tree,Vue中的虚拟Dom。

基于redux的新架构设计

redux是一个状态管理的方案,我认为wangEditor的新内核可以架设在redux工作流上。

流程图

明人不说暗话,先摆上流程图

image.png

主要角色

  • store:存储state数据的仓库
  • state:即文档协议(网上有很多种叫法,这里暂且称为Schema吧)
  • action:指一个操作,若想要改变state,action是唯一方式
  • reducer:接收state和action,返回一个新的state

流程分析

  1. EventEmitter监听事件,当监听到用户对编辑区内容进行操作时,触发一个action。
  2. EnentEmitter将生成的action交给store
  3. store将当前的state和action交给reducer
  4. reducer负责将action应用到当前的state,并返回一个新的state
  5. store用新state覆盖掉旧state
  6. store将state传给render
  7. render接收到state后,将state映射为真实DOM
  8. 将当前的DOM展示到页面上

特点

  1. 单向数据流
  2. state是immutable的
  3. 流程清晰,各部分工作分明。(也说明对插件的支持性强,可以在每一个工作节点暴露hook,扩展功能)

如何支持插件化

插件用于功能扩展,在该架构中,想实现功能扩展基本就两个工作:

  1. 设计一个合理的action。
  2. 设计一个处理该action的reducer,让action可以作用到state上。 比如想有加粗的功能:
  3. 在EventEmitter注册监听事件,生成一个特定的action
  4. 在reducer中注入处理action的代码 比如想有上传图片的功能:
  5. 在EventEmitter注册监听事件,生成一个特定的action
  6. 在reducer中注入处理action的代码 架构设计重要的地方出现了,如何才能设计一个合理的数据结构对功能的扩展非常重要! 还有就是,如果用户想进行二次开发的话,他需要先学习我们的数据结构,摸透了后才能进行设计,这是有学习成本的。

现在社区中,听说Draft(一个基于React的富文本编辑器)好像是说无需学习新的UI范式,只要你会React,就能进行二次开发,不知道咋实现的,后面调研一下。

细节

L1内核还是依赖contentededitable这个原生属性的。

这会出现一个问题:当在编辑区内直接修改内容时,好像是没有办法能阻止的(我不知道咋阻止,知道的小伙伴吱一声),那么此时store中的state和编辑区内容就不对应了,页面的渲染取决于两个因素:用户直接对编辑区做修改 或者 通过事件触发action

让我想解决方案的话,最好就是可以阻止页面渲染,通过生成action的方式来改变视图。 退而求其次的话,就是页面不阻止页面变化,然后同样生成action,让state结构和页面dom结构的映射关系保持一致。

补充:虽然已经想出具体的工作流,但很多细节还没有考虑。如Schema的数据结构应该是什么样的,reducer如何把Schema映射为一个真实的DOM等。这些等参考了社区中其它富文本编辑器的作法后再说哈哈。

写在最后

大佬们对文章有什么意见或有什么问题可以评论,有空我会回复的。

哎,终于冲完了,可以歇歇。(打把王者放松放松)。后面就没啥要分享的了,和大家唠唠嗑。

实话说,累是挺累的,不过也收获不少。调研时,在看社区中一些解决方案的时候,有种眼前一亮的感觉,还能这么玩!如果以后面试的时候有面试官问我怎么学习前端的,我直接回复:就是玩儿!

后面会找时间调研社区中其它编辑器是怎么做的,如quill,ProseMirror,Draft,slate。有了什么新收获,再来跟大家分享。

如果有看到这里的小伙伴,那你真厉害,我感觉这篇文章分量还蛮足的。如果你一路看过来的话,那你在富文本编辑器领域跟我一样,算是...入门吧哈哈。

还有就是,希望大家能坚持总结!不管在校还是工作了,都要坚持总结,沉淀下来的东西,才是自己的。每天学了一堆知识,木有总结的话,过段时间是会忘的,虽然总结了也会忘(没错说的就是我自己),但时刻记录当下的想法,以后回过头来看,可能会给你带来新的启发,这也是我坚持掘金写作的一个原因。

如果你有什么问题想私下联系我,可以加我的微信:YM_coke(qq好像没人加,我就不贴了)