Contribute to this guide

guide架构

本文假设您已经阅读了 “架构” 部分,该部分是 编辑引擎架构简介

# 快速回顾

编辑器的架构在 editor.model.schema 属性中可用。它定义了允许的模型结构(模型元素如何嵌套)、允许的属性(元素和文本节点的属性)以及其他特性(内联与块、外部操作的原子性)。此信息随后由编辑功能和编辑引擎用于确定如何处理模型、在何处启用功能等。

架构规则可以使用 Schema#register()Schema#extend() 方法定义。前者只能对给定项目名称使用一次,这确保只有一个编辑功能可以引入该项目。类似地,extend() 只能用于已定义的项目。

元素和属性由功能分别使用 Schema#checkChild()Schema#checkAttribute() 方法进行检查。

# 定义允许的结构

当功能引入模型元素时,它应该在架构中注册该元素。除了定义该元素可能存在于模型中之外,功能还需要定义该元素可以在何处放置。此信息由 allowIn 属性提供,该属性属于 SchemaItemDefinition

schema.register( 'myElement', {
    allowIn: '$root'
} );

这使架构知道 <myElement> 可以是 <$root> 的子项。$root 元素是编辑框架定义的通用节点之一。默认情况下,编辑器将主根元素命名为 <$root>,因此上述定义允许在主编辑器元素中使用 <myElement>

换句话说,这将是正确的

<$root>
    <myElement></myElement>
</$root>

而这将是不正确的

<$root>
    <foo>
        <myElement></myElement>
    </foo>
</$root>

要声明哪些节点允许在注册的元素内部,可以使用 allowChildren 属性

schema.register( 'myElement', {
    allowIn: '$root',
    allowChildren: '$text'
} );

要允许以下结构

<$root>
    <myElement>
        foobar
    </myElement>
</$root>

allowInallowChildren 属性都可以从其他 SchemaItemDefinition 项目继承。

您可以在 SchemaItemDefinition API 指南中了解更多有关项目定义格式的信息。

# 禁止结构

架构除了允许某些结构外,还可以用于确保某些结构被明确禁止。这可以通过使用禁止规则来实现。

通常,您将使用 disallowChildren 属性来实现这一点。它可用于定义哪些节点在给定元素内部是禁止的

schema.register( 'myElement', {
    inheritAllFrom: '$block',
    disallowChildren: 'imageInline'
} );

在上面的示例中,新的自定义元素应该像任何块级元素(段落、标题等)一样,但不能在其中插入内联图像。

# 优先级高于允许规则

通常,所有 disallow 规则的优先级都高于其 allow 对应规则。当我们还将继承考虑进来时,规则的层次结构如下所示(从最高优先级开始)

  1. 来自元素自身定义的 disallowChildren / disallowIn
  2. 来自元素自身定义的 allowChildren / allowIn
  3. 来自继承元素定义的 disallowChildren / disallowIn
  4. 来自继承元素定义的 allowChildren / allowIn

# 禁止规则示例

虽然禁止规则对于简单情况来说很容易理解,但在涉及更复杂的规则时,事情可能会变得不清楚。以下是一些示例,说明当涉及规则继承时,禁止规则是如何工作的。

schema.register( 'baseChild' );
schema.register( 'baseParent', { allowChildren: [ 'baseChild' ] } );

schema.register( 'extendedChild', { inheritAllFrom: 'baseChild' } );
schema.register( 'extendedParent', { inheritAllFrom: 'baseParent', disallowChildren: [ 'baseChild' ] } );

在这种情况下,extendedChild 将被允许在 baseParent 中(由于从 baseChild 继承)和在 extendedParent 中(因为它继承了 baseParent)。

baseChild 仅被允许在 baseParent 中。虽然 extendedParent 继承了 baseParent 的所有规则,但它明确禁止 baseChild 作为其定义的一部分。

下面是一个不同的示例,其中 baseChild 使用 disallowIn 规则进行扩展

schema.register( 'baseParent' );
schema.register( 'baseChild', { allowIn: 'baseParent' } );

schema.register( 'extendedParent', { inheritAllFrom: 'baseParent' } );
schema.register( 'extendedChild', { inheritAllFrom: 'baseChild' } );
schema.extend( 'baseChild', { disallowIn: 'extendedParent' } );

这改变了架构规则的解析方式。baseChild 仍然在 extendedParent 中被禁止,如前所述。但现在,extendedChild 也在 extendedParent 中被禁止了。这是因为它将从 baseChild 继承此规则,并且没有其他规则允许 extendedChildextendedParent 中。

当然,您也可以将 allowIndisallowChildren 以及 allowChildrendisallowIn 混合使用。

最后,可能会出现这种情况,您希望从一个已被禁止的项目继承,但新元素应该重新被允许。在这种情况下,定义应该如下所示

schema.register( 'baseParent', { inheritAllFrom: 'paragraph', disallowChildren: [ 'imageInline' ] } );
schema.register( 'extendedParent', { inheritAllFrom: 'baseParent', allowChildren: [ 'imageInline' ] } );

这里,imageInline 在段落中被允许,但不会被允许在 baseParent 中。但是,extendedParent 将再次重新允许它,因为自己的定义比继承的定义更重要。

# 定义其他语义

除了设置允许的结构外,架构还可以定义模型元素的其他特征。通过使用 is* 属性,功能作者可以声明某个元素应该如何被其他功能和引擎处理。

以下是列出各种模型元素及其在架构中注册的属性的表格

架构条目 定义中的属性
isBlock isLimit isObject isInline isSelectable isContent
$block true false false false false false
$container false false false false false false
$blockObject true true[1] true false true[2] true[3]
$inlineObject false true[1] true true true[2] true[3]
$clipboardHolder false true false false false false
$documentFragment false true false false false false
$marker false false false false false false
$root false true false false false false
$text false false false true false true
blockQuote false false false false false false
caption false true false false false false
codeBlock true false false false false false
heading1 true false false false false false
heading2 true false false false false false
heading3 true false false false false false
horizontalLine true true[1] true false true[2] true[3]
imageBlock true true[1] true false true[2] true[3]
imageInline false true[1] true true true[2] true[3]
listItem true false false false false false
media true true[1] true false true[2] true[3]
pageBreak true true[1] true false true[2] true[3]
paragraph true false false false false false
softBreak false false false true false false
table true true[1] true false true[2] true[3]
tableRow false true false false false false
tableCell false true false false true false

# 限制元素

考虑一个像图片标题这样的功能。标题文本区域应该构建对某些内部操作的边界

  • 在内部开始的选择不应该在外部结束。
  • 退格键删除键 不应删除该区域。按 回车键 不应分割该区域。

它也应该作为外部操作的边界。这主要是由一个选择后缀器强制执行的,它确保从外部开始的选择不应在内部结束。这意味着大多数操作将要么应用于此类元素的“外部”,要么应用于其内部的内容。

考虑到这些特性,图像标题应通过使用 isLimit 属性定义为一个限制元素。

schema.register( 'myCaption', {
    isLimit: true
} );

然后,引擎和各种功能通过 Schema#isLimit() 检查它,并可以相应地采取行动。

“限制元素”并不意味着“可编辑元素”。“可编辑元素”的概念保留给视图,并由 EditableElement 表达。

# 对象元素

对于上面示例中的图像标题,选择标题框然后将其复制或拖动到其他地方并没有多大意义。

没有它所描述的图像的标题毫无意义。然而,图像更自给自足。大多数情况下,用户应该能够选择整个图像(及其所有内部内容),然后将其复制或移动。应该使用 isObject 属性来标记这种行为。

schema.register( 'myImage', {
    isObject: true
} );

稍后可以使用 Schema#isObject() 来检查此属性。

还有 $blockObject$inlineObject 通用项,它们将 isObject 属性设置为 true。大多数对象类型项将从 $blockObject$inlineObject(通过 inheritAllFrom)继承。

每个对象也自动成为

# 块元素

一般来说,内容通常由段落、列表项、图像、标题等块组成。所有这些元素都应通过使用 isBlock 标记为块。

isBlock 属性设置为 true 的架构项(除其他外)会影响 Selection#getSelectedBlocks() 行为,从而允许将块级属性(如 alignment)设置为适当的元素。

重要的是要记住,块不应该允许另一个块在里面。容器元素(如 <blockQuote>,可以包含其他块元素)不应该被标记为块。

还有 $block$blockObject 通用项,它们将 isBlock 属性设置为 true。大多数块类型项将从 $block$blockObject(通过 inheritAllFrom)继承。

请注意,每个从 $block 继承的项都设置了 isBlock,但并非每个设置了 isBlock 的项都必须是 $block

# 内联元素

在编辑器中,所有 HTML 格式化元素(如 <strong><code>)都由文本属性表示。因此,内联模型元素不应该用于这些场景。

目前,isInline 属性用于 $text 令牌(即文本节点)和元素(如 <softBreak><imageInline> 或占位符元素,如 实现内联小部件 教程中所述)。

到目前为止,CKEditor 5 中对内联元素的支持仅限于自包含元素。因此,所有标记为 isInline 的元素也应标记为 isObject

还有 $inlineObject 通用项,它将 isInline 属性设置为 true。大多数内联对象类型项将从 $inlineObject(通过 inheritAllFrom)继承。

# 可选择元素

用户可以整体选择(及其所有内部内容)然后例如复制或应用格式的元素,在架构中标记为 isSelectable 属性

schema.register( 'mySelectable', {
    isSelectable: true
} );

稍后可以使用 Schema#isSelectable() 方法来检查此属性。

默认情况下,所有 对象元素 都可选择。但是,编辑器中还注册了其他可选择元素。例如,还有 tableCell 模型元素(在编辑视图中呈现为 <td>),该元素是可选择的,但注册为对象。 表格选择 插件利用了这一事实,允许用户创建由多个表格单元格组成的矩形选择。

# 内容元素

您可以通过查看内容模型元素在编辑器数据中的表示来区分它们与其他元素(您可以使用 editor.getData()Model#hasContent() 来检查)。

诸如图像或媒体之类的元素将始终找到进入编辑器数据的方法,这就是使它们成为内容元素的原因。它们在架构中标记为 isContent 属性

schema.register( 'myImage', {
    isContent: true
} );

稍后可以使用 Schema#isContent() 方法来检查此属性。

同时,段落、列表项或标题之类的元素不是内容元素,因为它们在编辑器输出中为空时会被跳过。从数据的角度来看,它们是透明的,除非它们包含其他内容元素(一个空段落与没有段落一样好)。

对象元素$text 默认情况下是内容。

# 通用项

有几个通用项(元素类)可用:$root$container$block$blockObject$inlineObject$text。它们定义如下

schema.register( '$root', {
    isLimit: true
} );

schema.register( '$container', {
    allowIn: [ '$root', '$container' ]
} );

schema.register( '$block', {
    allowIn: [ '$root', '$container' ],
    isBlock: true
} );

schema.register( '$blockObject', {
    allowWhere: '$block',
    isBlock: true,
    isObject: true
} );

schema.register( '$inlineObject', {
    allowWhere: '$text',
    allowAttributesOf: '$text',
    isInline: true,
    isObject: true
} );

schema.register( '$text', {
    allowIn: '$block',
    isInline: true,
    isContent: true
} );

然后,这些定义可以被功能重用,以便以更可扩展的方式创建它们自己的定义。例如,Paragraph 功能将定义其项为

schema.register( 'paragraph', {
    inheritAllFrom: '$block'
} );

这转化为

schema.register( 'paragraph', {
    allowWhere: '$block',
    allowContentOf: '$block',
    allowAttributesOf: '$block',
    inheritTypesFrom: '$block'
} );

这可以解释为

  • <paragraph> 元素将被允许在允许 <$block> 的元素中(如在 <$root> 中)。
  • <paragraph> 元素将允许在 <$block> 中允许的所有节点(如 $text)。
  • <paragraph> 元素将允许在 <$block> 中允许的所有属性。
  • <paragraph> 元素将继承 <$block> 的所有 is* 属性(如 isBlock)。

由于 <paragraph> 定义从 <$block> 继承,因此其他功能可以使用 <$block> 类型来间接扩展 <paragraph> 定义。例如,BlockQuote 功能就是这样做的

schema.register( 'blockQuote', {
    inheritAllFrom: '$container'
} );

因为 <$block><$container> 中被允许(参见 schema.register( '$block' ...)),尽管块引用和段落功能彼此一无所知,但段落将被允许在块引用中:架构规则允许链接。

更进一步,如果有人注册一个 <section> 元素(使用 allowContentOf: '$root' 规则),因为 <$container> 也在 <$root> 中被允许(参见 schema.register( '$container' ...)),<section> 元素将开箱即用地允许块引用。

您可以在 SchemaItemDefinition 中了解更多有关项定义格式的信息。

# 通用项之间的关系

通用项之间关系(哪些可以在哪里使用)可以通过以下抽象结构可视化

<$root>
    <$block>                <!-- example: <paragraph>, <heading1> -->
        <$text/>
        <$inlineObject/>    <!-- example: <imageInline> -->
    </$block>
    <$blockObject/>         <!-- example: <imageBlock>, <table> -->
    <$container>            <!-- example: <blockQuote> -->
        <$container/>
        <$block/>
        <$blockObject/>
    </$container>
</$root>

例如,以下模型内容将满足上述规则

<$root>
    <heading1>            <!-- inheritAllFrom: $block -->
        <$text/>          <!-- allowIn: $block -->
    </heading1>
    <paragraph>           <!-- inheritAllFrom: $block -->
        <$text/>          <!-- allowIn: $block -->
        <softBreak/>      <!-- allowWhere: $text -->
        <$text/>          <!-- allowIn: $block -->
        <imageInline/>    <!-- inheritAllFrom: $inlineObject -->
    </paragraph>
    <imageBlock>          <!-- inheritAllFrom: $blockObject -->
        <caption>         <!-- allowIn: imageBlock, allowContentOf: $block -->
            <$text/>      <!-- allowIn: $block -->
        </caption>
    </imageBlock>
    <blockQuote>                    <!-- inheritAllFrom: $container -->
        <paragraph/>                <!-- inheritAllFrom: $block -->
        <table>                     <!-- inheritAllFrom: $blockObject -->
            <tableRow>              <!-- allowIn: table -->
                <tableCell>         <!-- allowIn: tableRow, allowContentOf: $container -->
                    <paragraph>     <!-- inheritAllFrom: $block -->
                        <$text/>    <!-- allowIn: $block -->
                    </paragraph>
                </tableCell>
            </tableRow>
        </table>
    </blockQuote>
</$root>

这反过来又具有以下 语义

<$root>                   <!-- isLimit: true -->
    <heading1>            <!-- isBlock: true -->
        <$text/>          <!-- isInline: true, isContent: true -->
    </heading1>
    <paragraph>           <!-- isBlock: true -->
        <$text/>          <!-- isInline: true, isContent: true -->
        <softBreak/>      <!-- isInline: true -->
        <$text/>          <!-- isInline: true, isContent: true -->
        <imageInline/>    <!-- isInline: true, isObject: true -->
    </paragraph>
    <imageBlock>          <!-- isBlock: true, isObject: true -->
        <caption>         <!-- isLimit: true -->
            <$text/>      <!-- isInline: true, isContent: true -->
        </caption>
    </imageBlock>
    <blockQuote>
        <paragraph/>                <!-- isBlock: true -->
        <table>                     <!-- isBlock: true, isObject: true -->
            <tableRow>              <!-- isLimit: true -->
                <tableCell>         <!-- isLimit: true -->
                    <paragraph>     <!-- isBlock: true -->
                        <$text/>    <!-- isInline: true, isContent: true -->
                    </paragraph>
                </tableCell>
            </tableRow>
        </table>
    </blockQuote>
</$root>

# 使用回调定义高级规则

基础 声明性 SchemaItemDefinition API 本质上是有限的,一些自定义规则可能无法通过这种方式实现。

出于这个原因,还可以通过提供回调来定义架构检查。这使您能够灵活地实现您需要的任何逻辑。

这些回调可以为子项检查(模型结构检查)和属性检查设置。

请注意,回调优先于通过声明性 API 定义的规则,并且可以覆盖这些规则。

# 子项检查(结构检查)

使用 Schema#addChildCheck(),您可以提供函数回调来实现用于检查模型结构的特定高级规则。

您可以提供仅在检查特定子项时触发的回调,或者提供对架构执行的所有检查触发的通用回调。

下面是特定回调的示例,它禁止在代码块内使用内联图像

schema.addChildCheck( context => {
    if ( context.endsWith( 'codeBlock' ) ) {
        return false;
    }
}, 'imageInline' );

第二个参数('imageInline')指定回调仅在检查 imageInline 时使用。

您还可以使用回调强制允许给定项。例如,允许特殊的 $marker 项在任何地方都被允许

schema.addChildCheck( () => true, '$marker' );

请注意,回调可能会返回 truefalse 或无值 (undefined)。如果返回 truefalse,则表示已做出决定,并且不会检查进一步的回调或声明性规则。该项将被允许或禁止。如果未返回值,则进一步的检查将决定该项是否被允许。

在某些情况下,您可能需要定义一个通用侦听器,该侦听器将在每次架构检查时触发。

例如,要禁止所有块对象(例如表格)在块引用内,您可以定义以下回调

schema.addChildCheck( ( context, childDefinition ) => {
    if ( context.endsWith( 'blockQuote' ) && childDefinition.isBlock && childDefinition.isObject ) {
        return false;
    }
} );

上面的代码将在每次 checkChild() 调用时触发,为您提供更大的灵活性。但是,请记住,使用多个通用回调可能会对编辑器性能产生负面影响。

# 属性检查

类似地,您可以定义回调来检查给定属性在给定项上是否被允许或不被允许。

这次,您将使用 Schema#addAttributeCheck() 来提供回调。

例如,允许自定义属性 headingMarker 在所有标题上

schema.addAttributeCheck( ( context, attributeName ) => {
    const isHeading = context.last.name.startsWith( 'heading' );
    
    if ( isHeading ) {
        return true;
    }
}, 'headingMarker' );

通用回调也可用。例如,禁止所有标题内的文本上的格式属性(如粗体或斜体)

schema.addAttributeCheck( ( context, attributeName ) => {
    const parent = context.getItem( context.length - 2 );
    const insideHeading = parent && parent.name.startsWith( 'heading' );
    
    if ( insideHeading && context.endsWith( '$text' ) && schema.getAttributeProperties( attributeName ).isFormatting ) {
        return false;
    }
} );

与子项检查回调相关的所有说明也适用于属性回调。

# 实施其他约束

Schema 的功能仅限于简单的(原子)Schema#checkChild()Schema#checkAttribute() 检查,这是有意的。可以想象,schema 应该支持定义更复杂的规则,例如“元素 <x> 必须始终紧随 <y> 之后”。虽然创建可以将此类定义提供给 schema 的 API 是可行的,但遗憾的是,期望每个编辑功能在处理模型时都会考虑这些规则是不现实的。同样,期望 schema 和编辑引擎本身能够自动完成这些操作也是不现实的。

例如,让我们回到“元素 <x> 必须始终紧随 <y> 之后”规则,以及此初始内容

<$root>
    <x>foo</x>
    <y>bar[bom</y>
    <z>bom]bar</z>
</$root>

现在想象一下,用户按下“块引用”按钮。通常,它会用 <blockQuote> 元素包装两个选定的块(<y><z>

<$root>
    <x>foo</x>
    <blockQuote>
        <y>bar[bom</y>
        <z>bom]bar</z>
    </blockQuote>
</$root>

但是,事实证明,这会创建一个不正确的结构 - <x> 不再紧随 <y> 之后。

应该怎么做呢?至少有 4 种可能的解决方案:块引用功能在这种情况下不适用,应在 <x> 之后创建一个新的 <y><x> 应与 <y> 一起移至 <blockQuote> 中,反之亦然。

虽然这是一个相对简单的场景(不像大多数实时协作编辑场景),但事实证明,很难说应该发生什么,以及谁应该对此进行修复。

因此,如果您的编辑器需要实现此类规则,您应该通过 模型的后置处理程序 来修复不正确的内容,或者积极地防止这种情况(例如,通过禁用某些功能)。这意味着这些约束将由您的代码专门为您的场景定义,从而使它们的实现更容易。

总之,关于谁应该如何实现附加约束的答案是:您的功能或您的编辑器通过 CKEditor 5 API。

# 谁检查 schema?

CKEditor 5 API 公开了许多方法来处理(更改)模型。这可以通过 编写器 完成,通过诸如 Model#insertContent() 之类的函数,通过命令,等等。

# 低级 API

最低级别的 API 是编写器(准确地说,还有低于它的原始操作,但它们仅用于特殊情况)。它允许对内容进行原子更改,例如插入、删除、移动或拆分节点,设置和删除属性,等等。重要的是要注意,编写器不会阻止应用违反 schema 中定义规则的更改

造成这种情况的原因是,当您实现命令或任何其他功能时,您可能需要执行多个操作才能完成所有必要的更改。在此期间(这些原子操作之间)的状态可能是不正确的。编写器必须允许这种情况。

例如,您需要将 <foo><$root> 移动到 <bar> 中,并(同时)将其重命名为 <oof>。但是 schema 定义了 <oof> 不允许在 <$root> 中,而 <foo> 不允许在 <bar> 中。如果编写器检查 schema,无论 renamemove 操作的顺序如何,它都会报错。

您可以争辩说,引擎可以通过在 Model#change() 结束时检查 schema 来处理这种情况(它的工作原理类似于事务 - 状态需要在事务结束时正确)。但是,这种方法并没有被采用,因为存在以下问题

  • 如何在事务提交后修复内容?无法实现一种合理的启发式算法,这种算法不会从用户的角度破坏内容。
  • 模型在实时协作更改期间可能会变得无效。操作转换(尽管我们以丰富形式实现了它,具有 11 种操作类型而不是基本 3 种),确保了冲突解决和最终一致性,但不能保证模型的有效性。

因此,我们选择通过更具表现力和灵活性的 模型的后置处理程序,在个案基础上处理此类情况。此外,我们将检查 schema 的责任转移到功能上。它们可以在进行更改之前做出更好的决策。您可以在上面的 “实现附加约束” 部分中了解有关此内容的更多信息。

# 高级 API

其他更高级别的函数呢?我们建议所有构建在编写器之上的 API 应该检查 schema。

例如,Model#insertContent() 方法将确保插入的节点在它们插入的位置是允许的。它也可能尝试拆分插入容器(如果 schema 允许),如果这样做会使要插入的元素成为允许的,等等。

同样,命令 - 如果正确实现 - 被禁用,如果它们不应该在当前位置执行。

最后,schema 在从视图到模型的转换过程中(也称为“上移”)起着至关重要的作用。在此过程中,转换器会确定它们是否可以将特定的视图元素或属性转换为模型中的给定位置。因此,如果您尝试将不正确的数据加载到编辑器中,或者当您粘贴从另一个网站复制的内容时,数据的结构和属性会根据当前的 schema 规则进行调整。

某些功能可能缺少 schema 检查。如果您遇到这种情况,请随时 向我们报告