The core library is not an easy drop-in component—we are prioritizing modularity and customizeability over simplicity, with the hope that, in the future, people will distribute drop-in editors based on ProseMirror. As such, this is more of a lego set than a matchbox car.
WYSISYN编辑器 Prosemirror 入门
为什么选择prosemirror
编辑器一向是前端领域的一个难点,一款成熟的编辑器,需要涉及许多方面的东西。
到底有多少东西...这个可以看看掘金上一位大哥在知乎上的回答
至于为什么要踩这个天坑,是公司想要一个所见即所得的markdown编辑器,不需要markdown源码,要有用markdown语法一样的输入规则,最后还需要输出markdown文档作为存储,在次之上还需要一些制定的需求。这就要求这个选型应该是一个灵活,可配置模块化编辑器框架,而不是一个
开箱即可用
的一个应用
。在选型的时候,之前公司已经有人用
prosemirror
进行一些特殊编辑器的开发(然而那位同事在我没来之前就走了),同时考虑的还有slate.js
,上面那位大哥也有在掘金上发布过一篇文章。那为什么不选择slate.js
呢(另外还有个Draft.js
没有去了解过)。原因很简单,就是因为我们的技术栈是Vue
而不是React
。slate.js
依赖于React
作为视图层,作为一个Vue
应用,还是不想再专门引入一个React
来为slate.js
服务。综上的原因,就踩上了这个天坑。虽然我没有用过
slate.js
,但是根据热度以及在github上的star也好,活跃度也好,我觉得应该不会比slate.js
小,但是它能产出的编辑器,不会比slate.js
差。但正因为活跃度等原因,你在谷歌或者百度上搜索,是没有关于
prosemirror
的任何中文资料
的,我一度认为这个框架在国内就没人用,直到有一天在discuss看到了上面说的那位大佬的头像,我才知道原来国内还是有人用的。理所当然的,也不会有对应的中文文档,踩了坑也只能上discuss或者issue
搜索提问。但万幸的是,作者非常热心,几乎每一个问题都会回答你,就算是非常入门级的问题,这一点在开发上帮了我很多忙。以下的内容,几乎是官网的文档,通过自己理解和简化写下来的,有兴趣的可以去官网了解更加详细的内容。
prosemirror简介
如果你觉得
prosemirror
很陌生,那你也许听过大名鼎鼎的codemirror
。对,就是那个在浏览器上的代码编辑器,两个是同个作者,一位非常有实力的德国人Marijn。上面说到的slate
也是有些核心的概念例如schema
是来自于prosemirror
的。prosemirror
不是一个大而全的框架,甚至于你去npm上搜索prosemirror
压根没有这个包。prosemirror
由无数个小的模块组成,正如它官网上说的类似于乐高一样堆叠成一个健壮编辑器它的核心库有
prosemirror-model
:定义编辑器的文档模型,用来描述编辑器内容的数据结构prosemirror-state
:提供描述编辑器整个状态的数据结构,包括选择,以及从一个状态转移到下一个状态的事务处理系统。prosemirror-view
:实现一个用户界面组件,该组件在浏览器中将给定的编辑器状态显示为可编辑元素,并处理用户与该元素的交互。prosemirror-transform
:包含以可记录和重放的方式修改文档的功能,这是state模块中事务的基础,并使撤消历史记录和协作编辑成为可能。看完这些描述是不是感觉很熟悉,一个非常像
React
的一组核心库。他们构成了整个编辑器的基础。当然,除了核心库,还需要各种各样的库来实现快捷键prosemirror-commands
、编辑历史prosemirror-history
等等。实现一个小编辑器
这是一个功能非常有限的,只有一些基本的按键(例如
enter换行
、bacakspace删除
)等,然后我们再加上一个ctrl-z
撤回和ctrl-y
重做。这段代码,把content的内容转化为编辑器的初始文本,作为初始的编辑状态。只能够做简单的编辑,例如删除、撤回、换行等。
parser是什么?
我们来看看上面那段代码做了什么事情。首先,预定了一个
conetent
id的内容,这个在最后展示是不可见的,为的是把已有的html文档
先存在dom里。紧接着,通过DOMParse
解析顺着schema
(下面会说这是什么)这个html文本
,获得一个Node
类型的对象,这个对象就可以传入doc属性
作为一个初始的文本数据渲染成编辑器的可编辑文本。这里的
DOMParse
就是一个作为把DOM渲染成Node
对象的一个解析器。除了DOMParse
,还有一个解析器就是MarkdownParser
,专门把markdown文档转化为Node
类数据。那么有解析器,就有对应的序列器,调用
EditorState.JSON()
可以把当前状态的doc
序列化成JSON格式,便于存储。schema是什么?
schema
是一套描述文档和Dom之间的关联的一套转化规则,如何把DOm转化为Node
或者说Node
转化为Dom,这是个关键,下面是一个基本的标题的schema
这样就是一个描述一个标题的文本规则,不过没有这个文本规则,解析器或者序列器不知道如何去解析。任何一个在编辑器中出现的Dom以及任何一个需要转化成Dom的节点类型,都需要有一个对应的
schema
否则无法编译。schema
可以自行创建或者在现有的schema
上进行添加。一个健壮的schema
对每一个属性的设置都有较高的要求,在这里不举例子了,免得带偏,可以自行上官网学习。Node是什么?
Node
类构成了Prosemirror文档的节点树,它的子节点也是Node
类。Node
类并不能直接被改变,是一个持久的数据结构,类似于React
中的state
,需要通过apply
一个transaction
类才能够改变doc
的结构。而Node
的结构又非常像Virtual Dom
,都具有树型和递归,通过实例解构来描述Dom,而且prosemirror
也有自己一套高效的更新算法来转化Node
和Dom
Node
的属性非常多,比如在文档的位置、子节点的数量、节点大小、文本内容等等等等,在许多情况下,这些属性都为实现某些特定的功能提供了非常大的帮助。Transaction是什么?
transaction
是一个描述编辑器状态改变的一个数据类型。在Prosemirror
中,调用EditorView.updateState
可以更新整个编辑器的状态,就算是敲打一个空格,都必须要通过state进行更新。那么,如果每次都用DOMParse
创建新的Node
来形成新的state
,历史记录等东西必然不会保留,而且在Prosemirror
中,到真正调用EditorState.apply
的过程中,会经过很多的Plugins
(如果有的话)去加工这个transaction
,所以一定要经过EditorState.apply
去应用一个transaction
生成一个新的state
,接着调用,才可以真正改变整个编辑器的状态,并保存好整个的状态,在编辑的时候也是如此。我们可以先看看一个例子dispatchTransaction
实际上是在调用EditorState.apply
前的最后一个方法,这里也可以不调用dispatchTransaction
,默认进行了更新。在这里的作用是,每次更新(不管是编辑还是插入删除等操作)都会log一段文字,仅此而已。如果不进行apply和update的操作,将会报错。可以通过Editor.tr
获取实时的transaction
。keymap、历史记录
keymap
是键盘输入规则的插件,history是历史记录的插件,这个略过。核心内容总结
到此为止,核心内容就已经介绍完毕,当然,核心内容只能作为对
prosemirror
的一个浅显认知,好让我们在后续的编辑器开发的时候,不会不明白它到底是怎么的一个运作原理。现在缺少的有一些输入规则,有这些输入规则,才能像写markdown一样实现WYSIWYN编辑器,还有顶部的操作栏等等。这些都是编辑器的一部分,不过因为不是核心库,这里就不讲了。官方有一个
example-setup
一个设置样例,官方同样推荐通过这个样例来改造成符合我们需求的设置接下来,就让我们偷懒地实现一个markdown的编辑器。例子同样是来自于官网。
实现一个markdown编辑器
很简单,只需要把parser换成
defaultMarkdownParser
,plugins
用默认的设置就可以了,然后再用prosemirror-example-setup
的默认样式,一个WYSIWYN编辑器就完成了。当然这只是一个非常简单的markdown编辑器,官方给出的
defaultMarkdownParser
只是用的CommonMark
标准,很多的常用markdown语法都没有。我们可以从中进行非常多的自定义。总结
本篇文章简略地介绍了
prosemirror
的一些思想和核心内容,这只是涉及一些皮毛,并不是完全展现其魅力。在它的论坛上,有许多的开发者贡献了许多令人拍案叫好的插件或者成熟的编辑器,都非常值得去学习借鉴。希望能更加深入理解篇prosemirror
。