深入了解Quill (Parchment, Blots,Lifecycle)
引言:这是有关于QuillJs 和 它的第三方库Parchment的一系列博客的第一篇。接下来的文章将在我写完之后发布到下面链接当中。
- Parchment, Blots, and Lifecycle
- Containers - Creating a Mutliline Block
- Inline Embeds - Creating an @mention Blot
- 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";