前言
近期在整理富文本 Quill.js 相关的资料,并应用到项目上。由于富文本可学习与应用的场景蛮多的,并且有一定的复杂度,故会整理相对比较系统的系列文章,来让大家能快速上手,并了解原理。
基本概念
Parchment 是 Quill.js 的文档对象模型,类似 DOM 之于 web 页面的关系。Parchment 树是由多个 Blot 组成的,这也同样类似 DOM 树与 Node 的关系。Parchment 应用于结构、格式和内容,Attributes 提供轻量级的格式信息。
总结下来,Parchment 中存在 3 个核心概念:
- Parchment 文档对象模型;
- Blot 文档的基本构建单元;
- 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;
}
生命周期
- create,创建时会调用 Blot 静态 create 方法生成 dom node ,再执行构造函数生成实例。基类 ShadowBlot 默认实现了从 static tagName 属性中创建 dom node 的方法。所以多数场景,我们只需要设置 tagName 即可默认创建此节点,其他场景可自定义此方法
- update,通过在 scroll blot 中监听 dom mutation ,并遍历 mutation record ,更新子组件。通过 MutationObserver 监听属性、字符、子树变更。变更后可做一些事情:例如同步 blot 与 dom 之间的状态、约束 ContainerBlot 的子节点(allowedChildren)、对增加节点出发 blot.attach 生命周期、对已删除节点同步 blot.detach 生命周期;
- optimize,更新后立即执行以优化 dom 结构,例如空节点移除、相近文本节点合并、数据回收等
- 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 架构》