让我们继续探究富文本编辑器ProseMirror。

DirectEditorProps

ProseMirror文档在Web页面上的呈现是通过视图来管理的。其中一个核心的类是EditorView,一个例子:

let appState = {
  editor: EditorState.create({schema}),
  score: 0
}
let view = new EditorView(document.body, {
  state: appState.editor,
  dispatchTransaction(transaction) {
    update({type: "EDITOR_TRANSACTION", transaction})
  }
})

还记得上一篇介绍的,ProseMirror的文档是用EditorState来控制,然后通过[Transaction]来更新的吗?

创建EditorView的时候必须指定文档状态(就是上面例子中的state:appState属性),也可以挂载一个钩子dispatchTransaction,用来截获transaction,做一些中间操弄。

如果你对ReactJS有所了解的话,你一定会对stateprops这两个名词比较熟悉。ReactJS用state来存储内部数据的状态,而用props存储视图的一些属性。ProseMirror或多或少借鉴了这个开年。上文中创建EditorView时提到的statedispatchTransaction都是属于DirectEditorProps集合内的参数。

DirectEditorProps则是EditorProps的超集。EditorProps中定义了一系列属性用于DOM相关的和操作,比如事件处理器、剪贴板解析等等。ProseMirorr的Plugin可以提供EditorProps中的属性,从而影响ProseMirorr对文档和视图的操作。

但是这样一来,EditorView自身的DirectEditorProps中的属性,以及Plugin集合提供的EditorProps中的属性有重合的地方,到底依据哪个来决定视图的行为呢?EditorView提供了一个someProp方法,可以按次序(先EditorView然后依次注册的Plugin集合)以一定的逻辑(不同属性逻辑不同)遍历Props集合,来决定视图的行为。

DOM生成

ProseMirror的文档是一颗节点树,在生成视图的时候就要把这些节点树转化为相应的DOM结构。但是,ProseMirror并没有直接把文档节点树转化为DOM结构,而是在中间加了一层ViewDesc。

创建ViewDesc需要使用以下参数:

  • parent, 父ViewDesc
  • children, 子ViewDesc
  • dom, 该ViewDesc所对应的DOM节点
  • contentDOM,见下文

ViewDesc.dom含有一个pmViewDesc属性,指向该ViewDesc本身。contentDOM用于告诉ProseMirror在何处插入子ViewDesc。contentDOMdom可以是同一个DOM节点。

从ViewDesc派生出若干个子类,包括:

  • NodeViewDesc
  • MarkViewDesc
  • TextViewDesc

其中NodeViewDesc就是用来描述文档中的Node,它的子类TextViewDesc用来表述文档中的TextNode。MarkViewDesc用来描述TextNode所附带的Mark集合。值得注意的是,虽然在文档中,Mark集合是TextNode(或者其他inline内容)的属性,不参与到树形结构的构成,但是在ViewDesc中,由于是要和DOM树同步,所以MarkViewDesc参与树形结构的构成。

假如有下面一个文档:

  • paragraph
    • text (内容:“Hello, “)
    • text (内容:“world!",标记:strong)

转化为ViewDesc后,会变成:

  • NodeViewDesc (对应paragraph)
    • TextViewDesc (对应"Hello, “)
    • MarkViewDesc (对应strong)
      • TextViewDesc (对应"world!")

小结

其实我们可以把ViewDesc理解为VirtualDOM。ProseMirror通过ViewDesc来保持文档和DOM之间的同步。具体同步的过程如下所示:

       DOM event
       ↗         ↘
  EditorView    Transaction
       ↖         ↙
       new EditorState

ViewDesc决定了如何响应DOM事件,并形成相应的Transaction;另外当EditorState更新之后,ViewDesc又通过更新的EditorState来更新DOM结构。所以ViewDesc具有双向同步能力,从DOM中同步出ProseMirorr文档,反之亦然。