深入了解Quill (Parchment, Blots,Lifecycle)

引言:这是有关于QuillJs 和 它的第三方库Parchment的一系列博客的第一篇。接下来的文章将在我写完之后发布到下面链接当中。

  1. Parchment, Blots, and Lifecycle
  2. Containers - Creating a Mutliline Block
  3. Inline Embeds - Creating an @mention Blot
  4. Block Embeds - Creating a Custom Video Blot without an iFrame

Note: 这一系列文章的受众为旨在试图深入了解Quill和它的底层Parchment的人。如果你只是想要运行编辑器,让项目启起来, Quickstart Guide Cloning Medium with Parchment guide 可能对你更有帮助

Quill是什么

Quill是一个现代的富文本编辑器,它设计出来为了解决更好的富文本兼容性,更好的扩展性。由 Jason Chen 和 Byron Milligan 创建,在Salesforce开源。已经有数百家公司在项目中利用它创建快速、可靠、丰富的编辑体验。

Quill更像一个组件库,包含的组件有常见的格式选项,如粗体、斜体、罢工、下划线、自定义字体和颜色、分隔符、标题、内联代码、代码块、块引号、列表(项目符号、编号、复选框)、公式、图像以及嵌入式视频。

对于编辑器,你想要一些什么功能?

几个月前,我工作的公司Vanilla Forums开始为我们的产品规划一个新的编辑器。我们当前的编辑器支持多种不同的文本输入格式,包括

  • Markdown
  • BBCode
  • HTML
  • WYSIWYG HTML (用 iFrame去渲染内容)

对于所有这些不同的格式,有着不同的解析器,渲染器和JS解析语言。因此我们开始创建新的编辑器,用一种新的统一、丰富的编辑体验来取代他们。

我们选择Quill作为新编辑器的基础,因为它具有浏览器兼容性和可扩展性,但很快就意识到它不可能具备我们所需要的所有现成功能。值得注意的是缺少多行块类型结构,如块引号(缺少嵌套和多行支持)。我们还有一些其他的自定义需求,比如

我们还提供了一些扩展功能,包括丰富的链接嵌入、图像和视频的特殊格式选项和功能。

所以我开始学习羽毛笔及其底层的数据库羊皮纸。这一系列文章代表了我对羊皮纸和羽毛笔的理解。我不是项目的维护者,所以如果这里有错误,请您指出。

数据格式

Quill有两种数据格式 Parchment 和Delta

Quill并没有沿用dom原有的文档模型,而是抽象出了Parchment作为dom的文档模型。Parchment由许许多多个Blots构成,纯文本的是textBlot,内联块元素是inlineBlot.

Delta作为编辑器中数据的持久层,采用了相对扁平的JSON的数据格式。每一个delta中携带了操作,该操作可能影响到多个dom结点,用它来标识不同文章状态之间的差异(op算法,实时协同编辑,shareDb)上也有好处。

Blot是什么?

Blot是Quill最为强大的抽象之一,他们作为接口暴露给用户,让他们能够准确的修改dom而不直接去操纵dom。(维护一致性的数据结构,对于不同环境下 渲染出同样的文章的稳定性有好处)。每个Blot必须实现Blot接口,都由ShadowBlot继承而来。

为了方便的操纵Dom,每个Blot都有以下的特性。

.parent-包含此污点的污点。如果此Blot是顶级Blot,则父级将为null。

.prev-上一个兄弟Blot。如果此Blot是其父项下的第一个子项,则prev将为null。

.next-下一个兄弟Blot。如果此Blot是其父项下的最后一个子项,则next将为null。

.scroll-scroll是文章中的最上层。(一些对于文章内容,一些container也都由scroll创建)

.domNode-Parchment和DOM树一一对应,因此每个Blot都可以访问它所表示的节点。

Blot的生命周期

每个Blot都有几个“生命周期方法”,您可以覆盖这些方法,以便在特定的时间运行代码。(如 create...)

通常开发情况下,你会调用到你重写的这些方法。

Creation

创建一个Blot有很多方法,但是最常用的一般是 Parchment.create()

Blot.create()

每个Blot有一个 static create() 方法,来创建DOM结点,这是初始化dom结点的最好的方式。

初始化的dom并不是随意生成的,一般只有你调用之后才会创建在Blot里。

Blots并不总是必要的,当你复制的时候,复制的html结构将直接传递到 Parchment.create() . Parchment 会跳过调用 create() 方法直接用传过来的html结构,走到下一个生命周期中。

import Block from "quill/blots/block";
class ClickableSpan extends Inline {
    static tagName = "span";
    static className = "ClickableSpan";
    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.
        // E.G. the Node is currently <code class="ClickableSpan">{initialValue}</code>
        const node = super.create();
        // Set an attribute on the DOM Node.
        node.setAttribute("spellcheck", false);
        // Add an additional class
        node.classList.add("otherClass")
        // Returning <code class="ClickableSpan otherClass">{initialValue}</code>
        return node;
    // ...

constructor(domNode)

构造一个domNode (这个过程通常在 static create() ,而不是constructor ) 并创建一个Blot。

我认为 constructor 和 static create 承担着接近的功能。

这是注册事件侦听器或执行自定义操作的好地方,因为它直接创建domNode,所以你也可以在这里操作样式。

在构造方法调用之后,我们的Blot并不会直接添加到dom树或Parchment的dom结构中。

class ClickableSpan extends Inline {
    // ...
    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("ClickableSpan was clicked. Blot: ", this);
    // ...

Registration

Parchment保留着你所有注册的Blots为了便于你后续的创建。Parchment会暴露一个 Parchment.create() 方法来通过名字创建Blot 。

为了便于使用这个能力,你需要在Parchment中注册这个Blot .在Quill中最好用 Quill.register() 来注册,当然它也是去调用 Parchment.register() 。更多的相关信息可以去看这篇文章 Quill's excellent documentation .

import Quill from "quill";
// Our Blot from earlier
class ClickableSpan extends Inline { /* ... */ }
Quill.register(ClickableSpan);

确保Blot拥有唯一的标识

如何确保Parchment中的Blot是唯一的,你通过调用 Parchment.create(blotName) 来创建blot。

通常,Parchment通过以下两种方式之一来进行区分是不是同一个Blot。

By tagName

import Inline from "quill/blots/inline";
// Matches to <strong ...>...</strong>
class Bold extends Inline {
    static tagName = "strong";
    static blotName = "bold";
// Matches to <em ...>...</em>
class Italic extends Inline {
    static tagName = "em";
    static blotName = "italic";
// Matches to <em ...>...</em>
class AltItalic extends Inline {
    static tagName = "em";
    static blotName = "alt-italic";
    // Returns <em class="alt-italic">...</em>
    static create() {
        const node = super.create();
        node.classList.add("Italic--alt");
// ... Registration here

By className

// ... Bold and Italic Blot from the previous example.
// Matches to <em class="alt-italic">...</em>
class AltItalic extends Inline {
    static tagName = "em";