富文本编辑器 Quill.js 系列二:Parchment - 文档模型

前言

近期在整理富文本 Quill.js 相关的资料,并应用到项目上。由于富文本可学习与应用的场景蛮多的,并且有一定的复杂度,故会整理相对比较系统的系列文章,来让大家能快速上手,并了解原理。

基本概念

Parchment 是 Quill.js 的文档对象模型,类似 DOM 之于 web 页面的关系。Parchment 树是由多个 Blot 组成的,这也同样类似 DOM 树与 Node 的关系。Parchment 应用于结构、格式和内容,Attributes 提供轻量级的格式信息。

总结下来,Parchment 中存在 3 个核心概念:

  1. Parchment 文档对象模型;
  2. Blot 文档的基本构建单元;
  3. Attributes 对 Blot 的格式信息补充,其实与 HTML Node 上的 attibutes 类似。

Blot

继承关系图

Blot 实现上作为一个 Class 来使用,并且提供了一些基本实现:Block、Inline、Embed。我们扩展这些基本单元都是通过继承来实现。

内置 Blot 实现如下,自下而上为父类与子类间的集成关系。(红字为示例,实现在 quill.js 包,而非 Parchment 包内实现的)。

LeafBlot 为叶子节点,不能包含子 blot 了,常见的例如 文本、图片等;ContainerBlot 用于容器节点,常见的例如标题、加粗、斜体、滚动容器。

类型声明

Blot 类型声明如下:

class Blot {
  // 唯一标识,用于富文本进行构造实例
  static blotName: string;
  static className: string;
  // 应用到富文本的 DOM tagName,用于序列化数据输入时能定位到其构造函数
  static tagName: string;
  static scope: Scope;

  domNode: Node;
  prev: Blot | null;
  next: Blot | null;
  parent: Blot;

  // 创建一个对应的 DOM node
  static create(value?: any): Node;

  constructor(domNode: Node, value?: any);

  // 对于叶子节点,代表 blot 的 value() 返回值;
  // 对于父容器节点,代表子节点的 values 总和。
  length(): Number;

  // 对制定返回的信息进行一些操作.
  // 一些操作将会同步调用子节点的生命周期,例如 deletaAt/formatAt.
  deleteAt(index: number, length: number);
  formatAt(index: number, length: number, format: string, value: any);
  insertAt(index: number, text: string);
  insertAt(index: number, embed: string, value: any);

  // 返回当前节点 与 入参祖先节点的偏移量
  offset(ancestor: Blot = this.parent): number;
  
  // blot 更新时触发,可以是用户触发 or Api 触发。
  // context 是容器更新时透传用于所有 blot 执行是共享的上下文对象,便于信息互通。
  update(mutations: MutationRecord[], context: {[key: string]: any});

  // 生命周期函数,在 update 完成之后执行。
  // 不会更改文档的信息,主要是为了减少 DOM 产物数的复杂度.
  // context 是容器更新时透传用于所有 blot 执行是共享的上下文对象,便于信息互通。
  optimize(context: {[key: string]: any}): void;


  /** Leaf Blots only **/

  // 返回 domNode 对应 blot 表示的值
  static value(domNode): any;

  // Given location represented by node and offset from DOM Selection Range,
  // return index to that location.
  index(node: Node, offset: number): number;

  // Given index to location within blot, return node and offset representing
  // that location, consumable by DOM Selection Range
  position(index: number, inclusive: boolean): [Node, number];

  // Return value represented by this blot
  value(): any;


  /** Parent blots only **/

  // Whitelist array of Blots that can be direct children.
  static allowedChildren: Blot[];

  // Default child blot to be inserted if this blot becomes empty.
  static defaultChild: Registry.BlotConstructor;

  children: LinkedList<Blot>;

  // Called during construction, should fill its own children LinkedList.
  build();

  // Useful search functions for descendant(s), should not modify
  descendant(type: BlotClass, index: number, inclusive?: boolean): Blot;
  descendants(type: BlotClass, index: number, length: number): Blot[];


  /** Formattable blots only **/

  // Returns format values represented by domNode if it is this Blot's type
  // No checking that domNode is this Blot's type is required.
  static formats(domNode: Node);

  // Apply format to blot. Should not pass onto child or other blot.
  format(format: name, value: any);

  // Return formats represented by blot, including from Attributors.
  formats(): Object;
}

生命周期

  1. create,创建时会调用 Blot 静态 create 方法生成 dom node ,再执行构造函数生成实例。基类 ShadowBlot 默认实现了从 static tagName 属性中创建 dom node 的方法。所以多数场景,我们只需要设置 tagName 即可默认创建此节点,其他场景可自定义此方法
  2. update,通过在 scroll blot 中监听 dom mutation ,并遍历 mutation record ,更新子组件。通过 MutationObserver 监听属性、字符、子树变更。变更后可做一些事情:例如同步 blot 与 dom 之间的状态、约束 ContainerBlot 的子节点(allowedChildren)、对增加节点出发 blot.attach 生命周期、对已删除节点同步 blot.detach 生命周期;
  3. optimize,更新后立即执行以优化 dom 结构,例如空节点移除、相近文本节点合并、数据回收等
  4. attach & detach,顾名思义,分别在新建 与 删除时触发。

Attributor

用于方便操作属性更新的工具,例如在 quill.js 中操作颜色等 style 属性、缩进等 class name 属性。

Parchment

处理 blot 生命周期的管理器,同时也是 blot/attributor 注册中心,全局单例。

通过 Parchment 统一的中间人管理,可以约束我们的 Blot or Attributor 写法,类似 “外观模式” 这一设计思想。

为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

总结

回顾一下,我们本篇文章开始初步了解了富文本编辑器 Quill.js 的核心组成 Parchment,了解是它构成了富文本的对象模型,管理着 dom,并双向同步信息。再后来我们了解了基本组成单元 Blot、Attibutor、Parchment,了解 3者的互相功能,当然占大头的是需要着重了解的 Blot,这对我们后续开发自定义富文本节点非常关键。

此文为富文本编辑器 Quill 系列文章,后续敬请期待《富文本编辑器 Quill.js 系列三:Quill.js 架构》

参考