一、前言
目前所有技术中,想要实现一款自己的编辑器,最简单的方式就是前端的web技术。
但即使这已经是最简单的方式了,但实际上想要自己从零写出一款功能俱全的编辑器依旧是一个非常大工程量。
至少经过我近半个月来的实践来说,其中的工作量是远超个人所能付出的经历的。
随即我便又开始寻找是否有现成的、高度可定制的开源项目,经过许久的寻觅,最终也找到了几款,但大多都是已经初具规模、想要进一步对其做出自定义开发仍旧是一件很困难的事情。
最终我的目光锁定到了本文所要介绍的开源库:ProseMiior。
这个开源库完全贯彻组件化开发的理念,绝对的高度可定义化,即使你想要使用它来实现一个最基本的编辑器,都需要对其有一定的了解才行。
前端过于基础的内容本文就不再赘述了,我使用的是vue3技术,直接运行命令npm create vue@latest
创建一个vue3项目即可,还不会vue3的可以参考文章:认识vue
二、核心概念
prosemirror本身是一个完全组件化的项目,其核心模块分为4个部分:
- prosemirror-model:定义了编辑器的 Document Model, 它用来描述编辑器的内容.
- prosemirror-state:提供了一个描述编辑器完整状态的单一数据结构, 包括编辑器的选区操作, 和一个用来处理从当前状态到下一个状态的一个事务处理系统.
- prosemirror-view 用来将给定的 state 展示成相对应的可编辑元素显示在编辑器中, 同时处理用户交互.
- prosemirror-transform:包含了一种可以被重做和撤销的修改文档的功能, 它是
prosemirror-state
库的事务处理功能的基础, 这使得撤销操作历史记录和协同编辑成为可能.
看起来比较抽象,如果没有实践的话恐怕很难理解它的行为,所以下面我们直接在代码中理解它。
首先想要使用这个库,你得先安装:
npm i prosemirror-view
这个view模块又是这四个模块中最核心的模块,你只需要下载好它,其它三个模块也会被自动下载下来。
除此之外我们还需要下载一个示例代码,因为如果不用该示例代码,我们的编辑器短时间内是无法使用的:
npm i prosemirror-schema-basic
然后清理掉vue自动生成的代码,直接在App.vue
根组件中写如下代码:
注意这里的写法,首先我们通过引出的示例scheme
来创建了一个EditorState
,也就是首先我们创建了一个State
模块,它用于表示我们编辑器的状态信息。
let state = EditorState.create({ schema })
然后下一步,通过这个状态,我们就能创建一个View
:
let mdedit = document.querySelector('.MdEdit');
let view = new EditorView(mdedit, {state});
它的第一个参数是要将编辑器绑定在哪个标签之下,第二个参数对象便放置着前面创建的state
键值相同的情况下可以进行缩写,上面的代码实际上等价于:
{state:state}
此时运行代码,就可以发现它已经能够正常编辑了:
但很遗憾,它目前只能输入字符,即使你按Enter
键,它都是没有任何反应的。
虽然上面的代码看上去只用到了核心模块中的两个:state
与view
,但实际上model
是其内部基本数据组织的形式,其内部默认使用了,所以大多数时候都不需要我们操行。
至于transform
同样也使用了其内部默认的代码,但我们却是可以定制自己的transform
:
这可以在创建View时传入一个函数来实现拦截的行为,dispatch Transaction
意思就是分发事务,事务对象通过参数传递了进来。
此时我们只需要做一件事情:通过view上的state来应用这个事务,也就是这里调用的apple
函数,就会得到一个新的state,然后我们在通过view上的updateState函数来更新这个state,就能完成数据的传入。
至于上面的日志信息只是方便查看的,效果如下:
这里显示的大小包含的不仅仅是文本,还有节点:
所以结果会比看到的日志多2
。
而如果你将updateState
代码注释掉:
dispatchTransaction(transaction) {
console.log("Document size went from", transaction.before.content.size,
"to", transaction.doc.content.size)
let newState = view.state.apply(transaction)
// view.updateState(newState)
}
此时你就会发现,现在无论你输入什么,都不会显示内容了。
而这就是state
的作用,因为没有应用新的state,所以一直保持着旧state,界面自然不会发生任何变化了。
这里简单再将其总结一下:
state
代表着编辑器的一个状态,其中包含了编辑器的一切状态。view
可以看作是要显示的内容。transaction
可以看作是行为,比如用户输入内容等任何操作都会首先产生一个transaction
,然后让当前的state
根据这个transaction
就能生成编辑器的下一个state,最后让view更新这个新的state就能完成界面的更新。
更简单的来说,就是用户所有的操作行为(transaction),都是根据当前编辑器界面(view)状态(state)产生的,通过当前状态、用户的行为,就能计算出下一刻编辑器应该有的新状态,最后将新状态更新到页面上即可完成页面的刷新。
三、应用插件
本文的最后,再来简单使用一下它提供的现成插件,来感受一下它强大的插件系统。
比如此时你会发现,即使是最简单的撤销、恢复两个功能目前我们的编辑器都是没有的,但好在官方提供了相关的插件:
npm i prosemirror-history
npm i prosemirror-keymap
上面两个插件分别代表了历史功能、键盘映射功能。
比如快捷键Ctrl+Z
我们一般就是想要撤销操作,就需要使用keymap
,但想要能够撤销肯定是需要将其历史行为进行记录的,所以要使用history
。
此时我们的代码就可以修改为如下形式:
注意插件应用的位置,是在创建state时传入的,只需要调用这两个包中提供的相关函数就能自动构造出插件对象。
我这里报错的原因是我的项目使用了typescript,由于这两个插件可能没有导出类型信息,又或者我这里出了毛病,即使下载了类型也报错,但这种类型错误并不影响使用,这里就不管它了。
比如先传入history()
构造一个历史对象的插件,然后再通过keymap
构造相关的快捷键以及快捷键所对应的执行函数。
这里快捷键的写法可以参考文章:ProseMirror Reference manual。
事实上你完全可以换作Ctrl-z
,与这里是一个效果。
此时运行代码,就会发现我们的编辑器就已经拥有撤销、恢复的功能了,并且分别对应于Ctrl+Z
与Ctrl+Y
这两个快捷键。