深入浅出QuillJS 第二节-Parchment抽象文档

1,182 阅读5分钟

Parchment是Quill非常核心的概念,Parchment核心代码并不在Quil的源码中,在另外一个Github地址中。俗话说:“不能把架构图画出来的,都没有真正理解框架真谛”。话不多说,先上架构图。

一.Parchment架构解析

上面简单画了一下整体Parchment一个类的结构图,以上是基于最新的dev2.0版本的Parchment结构,我目前研究和应用都是基于这个版本。包括我用的quill也是Dev2.0版本。

由图看出Blot大概可以分成两种。第一种是继承ParentBlot的

ContainerBlot 【容器节点】
ScrollBlot root【文档的根节点,不可格式化】
BlockBlot 块级 【可格式化的父级节点】
InlineBlot 内联 【可格式化的父级节点】
第二种是继承LeafBlot的 EmbedBlot 嵌入式节点 和 TextBlot 文本

两种类型的主要区别在于继承LeafBlot的节点是都属于一个原子节点,不可用做Parent节点,包括整个继承关系里面也可以看出来。所以leaf的节点,没有对child节点操作的方法。这一点可以从源码里面就可以看出来。

下面我讲几个主要的Blot.

1 .ShadowBlot

从图中可以看出,几乎所有的Blot的都是来源自ShadowBlot,所以它是整个Parchment的核心类,里面实现BLot接口大部分公用属性和方法,包含默认的生命周期的实现。下面介绍几个关键的函数实现。

  • create() 节点创建和初始化的操作,根据用户设定的TagName创建一个真实的element 并且添加 className,并且返回一个dom节点
  • formatAt(index,length,name,value) 默认的格式化行为, 会根据index和length格式化一个范围,源码看的话,会找index和length之间的blot。 给这个blot做一个wrap操作。如果没有Parent的会根据name和value生成的blot,进行insertBefore向前插入。如果有parent就进行了appendChild操作。
  • optimize() Shadow里 的optimize的作用主要是为了监测当前元素是否有requiredContainer,requiredContainer关联的是ContainerBlot。如果有的话,会自动包裹这个ContainerBlot,形成一个嵌套。 后面讲Blot嵌套会着重介绍

2. ParentBlot

parentBlot从名字可以理解,这个是用于父节点的blot 。 能对子节点进行增,删,改,移动,查找 。

  • build()根据从当前的dom 构建children节点数组。
  • appendChild(child) 添加子节点
  • moveChildren(target) 移动子节点到目标父节点中
  • removeChild(child) 删除某个节点

3.ContainerBlot

容器节点,顾名思义,这个是包裹其他节点的节点。但这里有一个关键知识点,ContainerBlot在quill中是不允许单独用updateContents方法插入的,我们每次插入的是他的子节点。 上面我讲过ShadowBlot中的optimize方法,它会自动找当前节点的容器节点。 找到后会进行wrap操作, 形成一个嵌套。

shadowBlot中optimize实现

4.ScrollBlot

ScrollBlot也是一个很重要的node, 他不是用户定义的node,编辑器运行时,会给当前编辑器内的每个Blot 内的属性scroll进行赋值。 这个是引用,他是文档的root node,用于最外层的插入和修改操作。也存储了一些编辑器的上下文信息。用于在任意Blot内部使用。最新的2.0版本中他可以直接操作registry,用注册或创建新的Blot。

二.Parchment的生命周期

画个简图描述Blot的生命周期

1. Register,每个Blot都需要先注册 ,才能使用

class MyCustomBlot extends Block { /* ... */ }

Quill.register(MyCustomBlot);

2 . Create ,每一个Blot都有static create()函数,用于根据初始值创建DOM Node。这里也非常适合在node上设置一些与Blot实例无关的初始属性。该函数会返回新创建的DOM Node,但并未插入文档中。此时,Blot也还未实例化成功,因为Blot实例化需要依赖DOM Node。

import Block from "quill/blots/block";

class MyCustomBlot extends Block {

    static create(initialValue) {
        // Allow the parent create function to give us a DOM Node
        // The DOM Node will be based on the provided tagName and className.
        const node = super.create();
        // Set an attribute on the DOM Node.
        node.setAttribute("id", "1");

        // Add an additional class
        node.classList.add("otherClass")
        return node;
    }

    // ...
}

3 .Constructor Blot的构造方法,通过domNode实例化blot。在这里可以做一些通常在class的构造函数中做的事情,比如:事件绑定,变量初始化,节点模型数据同步等等。

class MyCustomBlot extends Block {

    constructor(domNode) {
        super(domNode);

        // Bind our click handler to the class.
        this.clickHandler = this.clickHandler.bind(this);
        domNode.addEventListener(this.clickHandler);
    }

    clickHandler(event) {
        console.log("MyCustomBlot was clicked. Blot: ", this);
    }

}

4. Build , 我们一般不会直接操作build, 这个由Blot自己调用,具体在ParentBlot源码中有体现。但leaf节点是没有这个操作的。但这个概念还是需要知道的。他会构建一个Children的关系,维护在Blot中。

public build(): void {
    this.children = new LinkedList<Blot>();
    // Need to be reversed for if DOM nodes already in order
    Array.from(this.domNode.childNodes)
      .filter((node: Node) => node !== this.uiNode)
      .reverse()
      .forEach((node: Node) => {
        try {
          const child = makeAttachedBlot(node, this.scroll);
          this.insertBefore(child, this.children.head || undefined);
        } catch (err) {
          if (err instanceof ParchmentError) {
            return;
          } else {
            throw err;
          }
        }
      });
}

5. update node更新操作,当Blot改变是调用,伴随着改变记录的保存 。这个是内部私有方法,我们一般不去重写它。同样Leaf node 没有这个操作。代码里面 update也是主要是处理Children新增和修改。所以leaf node没必要使用。

6. optimize 同样Blot有任何变动,都会触发optimize方法,它是支持重写的,leaf也支持这个方法。一般做一些最后的校验或限定操作,比如表格插入数据后,重写计算格子大小。 比如更新完成后,对数据结果进行实时保存。避免在optimize方法中改变document的length和value。该方法中很适合做一些降低document复杂度的事。

三.结语

 下一节, 我们讲一讲Quill 另一个重要的概念delta,一些delta常用基本操作,并且探索delta这个偏平化的JSON是怎么巧妙的实现嵌套的。Quill是怎么解析delta的。delta与html怎么互转等等。