格式

区块编辑器文章是文章的正确区块感知表示形式:一组语义一致的描述,说明每个区块是什么以及它的核心数据是什么。这种表示形式只存在于内存中。它就是排字车间中的版框,随着活字被加入和重新定位而不断变化。

区块编辑器文章并不是它所产生的工件,即 post_content。后者是印刷出来的页面,为读者做了优化,但保留了隐形的标记以供日后编辑。

区块编辑器的输入和输出是采用以下当前格式的区块对象树:

const value = [ block1, block2, block3 ];

区块对象

每个区块对象都有一个 id、一组属性,以及可能的子区块列表。

const block = {
    clientId, // 唯一的字符串标识符。
    type, // 区块类型(段落、图像……)
    attributes, // 表示当前区块直接属性/内容的 (key, value) 属性集合。
    innerBlocks, // 子区块或内部区块的数组。
};

请注意,属性的键和类型以及允许的内部区块都是由区块类型定义的。例如,核心引用区块有一个 cite 字符串属性,表示引用的内容;而标题区块有一个数值型的 level 属性,表示标题的级别(1 到 6)。

在编辑器中区块的生命周期内,区块对象可以接收额外的元数据:

  • isValid:一个布尔值,表示区块是否有效;
  • originalContent:区块的原始 HTML 序列化内容。

示例

// 一个简单的段落区块。
const paragraphBlock1 = {
    clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a3',
    type: 'core/paragraph',
    attributes: {
        content: 'This is the <strong>content</strong> of the paragraph block',
        dropCap: true,
    },
};

// 一个分隔符区块。
const separatorBlock = {
    clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a4',
    type: 'core/separator',
    attributes: {},
};

// 一个列区块,其中每一列都包含一个段落区块。
const columnsBlock = {
    clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a7',
    type: 'core/columns',
    attributes: {},
    innerBlocks: [
        {
            clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a5',
            type: 'core/column',
            attributes: {},
            innerBlocks: [ paragraphBlock1 ],
        },
        {
            clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a6',
            type: 'core/column',
            attributes: {},
            innerBlocks: [ paragraphBlock2 ],
        },
    ],
};

序列化与解析

然而,该数据模型是编辑文章时驻留在内存中的内容。渲染时,页面查看者是看不到它的,就像印刷出来的页面上不会留有在印刷机上形成它的字母结构的任何痕迹一样。

由于整个 WordPress 生态系统在渲染或编辑文章时期望接收到的是 HTML,区块编辑器会通过序列化将其数据转换为可以保存到 post_content 中的内容。这确保了内容拥有单一的事实来源,并且该来源保持可读,与目前所有与 WordPress 内容交互的工具兼容。如果我们将对象树单独存储,就会面临 post_content 与该树失去同步的风险,以及数据在两处重复的问题。

因此,序列化过程会使用 HTML 注释作为显式的区块定界符——注释中可以包含非 HTML 形式的属性——将区块树转换为 HTML。这就好比在印刷页面上印上看不见的标记,留下原始结构化意图的痕迹。

这是流程的一端。另一端则是当文章需要再次编辑时如何重建区块集合。形式化文法定义了区块编辑器文章的序列化表示应如何加载,正如一些基本规则定义了如何将树转换为类似 HTML 的字符串一样。区块编辑器的文章并非为手动编辑而设计;它们也不是为作为 HTML 文档来编辑而设计的,因为区块编辑器文章本质上并不是 HTML。

它们只是碰巧以这样一种方式存储在 post_content 中:无需任何转换即可被任何旧式系统查看。诚然,在没有相应机制的情况下将存储的 HTML 加载到浏览器中可能会降低体验;而且如果其中包含动态内容区块,动态元素可能无法加载,服务器生成的内容可能不会显示,交互式内容也可能保持静态。然而,这至少可以避免在对区块无感知的主题和安装上无法查看区块编辑器文章的情况,并且提供了访问内容的最简便方式。换句话说,即使保存的 HTML 按原样渲染,文章也基本保持完好。

定界符与解析表达式文法

我们转而尝试找到一种方法,在现有的 HTML 语法中保持形式性、明确性和无歧义性。在 HTML 内部有多种可选方案。

在这些选项中,有人提出了一种新颖的方法:通过将数据存储在 HTML 注释中,我们可以确信不会破坏文档中其余的 HTML,浏览器会忽略它,而且我们可以简化解析文档的方法。

HTML 注释的独特之处在于,它们不能合法地存在于有歧义的位置,例如 HTML 属性内部,比如 <img alt='data-id="14"'>。注释也非常宽松。HTML 属性的正确解析相当复杂,而注释则很容易描述:以 <!-- 开头,后跟除 -- 之外的任何内容,直到第一个 --> 为止。这种简单性和宽松性意味着,解析器可以用多种方式实现,而无需完全理解 HTML;而且我们可以自由地在注释内部使用更方便的语法——只需要转义双连字符序列即可。我们在存储区块属性时便利用了这一点:将其作为注释内部的 JSON 字面量。

经过解析器处理之后,我们得到一个可以按惯用方式操作的简单对象,而且不必担心对数据进行转义或反转义。这已经由序列化过程为我们处理好了。由于注释与其他 HTML 标签截然不同,而且我们可以先执行一遍处理来提取顶层区块,我们实际上并不依赖于完全有效的 HTML!

这对我们如何将解析器做得简单而高效具有重大意义。这些显式边界还可以防止单个区块中的损坏蔓延到其他区块或损害整个文档。它还允许系统在渲染之前检测出无法识别的区块。

注意:区块的定义性特征在于其语义及其提供的隔离机制:换言之,就是它们的身份。另一方面,数据存储在哪里则是一个更宽松的方面。区块不仅支持静态本地数据(通过 HTML 注释内部或区块 HTML 中的 JSON 字面量),还预计会出现更多机制(例如全局区块,或以其他方式借助互补的 WP_Post 对象进行存储)。详情请参见属性。

序列化区块的剖析

当区块在编辑会话结束后保存到内容中时,其属性(取决于区块的性质)会被序列化到这些显式的注释定界符中。

<!-- wp:image -->
<figure class="wp-block-image"><img src="source.jpg" alt="" /></figure>
<!-- /wp:image -->

一个在显示之前需要由服务器渲染的纯动态区块可能如下所示:

<!-- wp:latest-posts {"postsToShow":4,"displayPostDate":true} /-->

数据生命周期

总而言之,区块编辑器工作流会借助标记定界符,将保存的文档解析为内存中的区块树。在编辑期间,所有操作都在区块树中进行。该过程最后将区块序列化回 post_content。

该工作流过程依赖一对序列化器/解析器来持久化保存文章。在假设的情况下,文章数据结构也可以使用插件来存储,或者从远程 JSON 文件获取后再转换为区块树。