Contribute to this guide

指南核心编辑器架构

The @ckeditor/ckeditor5-core 包比较简单。它只包含几个类。您需要了解的类如下所示。

# 编辑器类

The Editor 类表示编辑器的基础。它是应用程序的入口点,将所有其他组件粘合在一起。它提供了一些您需要了解的属性。

  • config – 配置对象。
  • pluginscommands – 加载的插件和命令的集合。
  • model – 编辑器数据模型的入口点。
  • data – 数据控制器。它控制如何从文档中检索数据并将其设置在文档中。
  • editing – 编辑控制器。它控制如何将模型渲染给用户进行编辑。
  • keystrokes – 按键处理程序。它允许将按键绑定到操作。

除此之外,编辑器还公开了一些方法。

  • create() – 静态 create() 方法。编辑器构造函数是受保护的,您应该使用此静态方法创建编辑器。它允许初始化过程是异步的。
  • destroy() – 销毁编辑器。
  • execute() – 执行给定的命令。
  • setData()getData() – 从编辑器中检索数据和将数据设置到编辑器中的方法。数据格式由数据控制器的 数据处理器 控制。它不需要是字符串(例如,如果您实现了这样的 数据处理器,它可以是 JSON)。例如,请查看如何 生成 Markdown 输出

有关方法的完整列表,请查看您使用的编辑器类的 API 文档。特定的编辑器实现可能提供其他方法。

The Editor 类是实现您自己的编辑器的基础。CKEditor 5 框架带有一些编辑器类型(例如,经典内联气球解耦),但您可以自由地实现工作和外观完全不同的编辑器。唯一的要求是您必须实现 Editor 接口。

# 插件

插件是引入编辑器功能的一种方式。在 CKEditor 5 中,即使 输入 也是一个插件。更重要的是,Typing 插件依赖于 InputDelete 插件,它们分别负责处理插入文本和删除内容的方法。同时,一些插件需要在某些情况下自定义 Backspace 行为并自行处理。这使得基本插件无需任何非通用知识。

现有 CKEditor 5 插件的实现方式的另一个重要方面是将引擎和 UI 部分分离。例如,BoldEditing 插件引入了模式定义、渲染 <strong> 标签的机制、应用和删除文本粗体的命令,而 BoldUI 插件添加了该功能的 UI(即按钮)。此功能分离旨在实现更大的重用(可以采用引擎部分并为功能实现自己的 UI),以及在服务器端运行 CKEditor 5。最后,有 Bold 插件,它为完整的体验带来了这两个插件。

总结一下,

  • 每个功能都是由插件实现或至少启用的。
  • 插件非常细粒度。
  • 插件了解编辑器的所有内容。
  • 插件应该尽可能少地了解其他插件。

这些是实现官方插件所基于的规则。在实现自己的插件时,如果您不打算发布它们,可以将此列表缩减到第一点。

在进行了冗长的介绍(旨在让您更容易理解现有插件)之后,可以解释插件 API。

所有插件都需要实现 PluginInterface。最简单的方法是继承自 Plugin 类。插件初始化代码应位于 init() 方法中(该方法可以返回一个 promise)。如果某些代码需要在其他插件初始化后执行,可以将其放在 afterInit() 方法中。插件之间的依赖关系是使用静态 requires 属性实现的。

import MyDependency from 'some/other/plugin';

class MyPlugin extends Plugin {
    static get requires() {
        return [ MyDependency ];
    }

    init() {
        // Initialize your plugin here.

        this.editor; // The editor instance which loaded this plugin.
    }
}

您可以在 分步教程 中看到如何实现简单的插件。

# 命令

命令是动作(回调)和状态(一组属性)的组合。例如,bold 命令对选定文本应用或删除粗体属性。如果放置选定内容的文本已经应用了粗体,则该命令的值为 true,否则为 false。如果 bold 命令可以在当前选定内容上执行,则它处于启用状态。如果不是(例如,由于此处的粗体不允许),则它处于禁用状态。

我们建议您使用官方 CKEditor 5 检查器 进行开发和调试。它将为您提供有关编辑器状态的大量有用信息,例如内部数据结构、选定内容、命令等等。

所有命令都需要继承自 Command 类。命令需要添加到编辑器的 命令集合 中,以便可以使用 Editor#execute() 方法执行它们。

举个例子

class MyCommand extends Command {
    execute( message ) {
        console.log( message );
    }
}

class MyPlugin extends Plugin {
    init() {
        const editor = this.editor;

        editor.commands.add( 'myCommand', new MyCommand( editor ) );
    }
}

调用 editor.execute( 'myCommand', 'Foo!' ) 将在控制台中记录 Foo!

要查看典型命令(如 bold)的状态管理是如何实现的,请查看 AttributeCommand 类的部分代码,该类是 bold 的基础。

首先要注意的是 refresh() 方法。

refresh() {
    const doc = this.editor.document;

    this.value = doc.selection.hasAttribute( this.attributeKey );
    this.isEnabled = doc.schema.checkAttributeInSelection(
        doc.selection, this.attributeKey
    );
}

对模型应用任何更改 时,此方法会自动(由命令本身)调用。这意味着,只要编辑器中的任何内容发生更改,该命令就会自动刷新其自身状态。

命令的重要一点是,它们的每个状态更改以及调用 execute() 方法都会触发事件。这些事件的一些示例包括 #set:value#change:value(当您更改 #value 属性时)以及 #execute(当您执行该命令时)。

可观察对象 深入研究指南中阅读有关此机制的更多信息。

这些事件使得可以从外部控制命令。例如,如果您想在某些条件为真时阻止特定命令(例如,根据您的应用程序逻辑,它们应该暂时不可用),并且没有其他更干净的方法,则可以手动阻止该命令。

function disableCommand( cmd ) {
    cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );

    cmd.isEnabled = false;

    // Make it possible to enable the command again.
    return () => {
        cmd.off( 'set:isEnabled', forceDisable );
        cmd.refresh();
    };

    function forceDisable( evt ) {
        evt.return = false;
        evt.stop();
    }
}

// Usage:

// Disabling the command.
const enableBold = disableCommand( editor.commands.get( 'bold' ) );

// Enabling the command again.
enableBold();

只要您不 关闭 此侦听器,该命令就会被阻止,而不管 someCommand.refresh() 调用了多少次。

默认情况下,当编辑器处于 只读 模式时,编辑器命令会被阻止。但是,如果您的命令不会更改编辑器数据,并且您希望它在只读模式下保持启用状态,则可以将 affectsData 标志设置为 false

class MyAlwaysEnabledCommand extends Command {
    constructor( editor ) {
        super( editor );

        // This command will remain enabled even when the editor is read-only.
        this.affectsData = false;
    }
}

The affectsData 标志也会影响 其他限制用户写入权限的编辑器模式 中的命令。

默认情况下,affectsData 标志对所有编辑器命令都设置为 true,并且除非您的命令应该在编辑器处于只读模式时启用,否则您无需更改它。该标志在编辑器的整个生命周期中是不可变的。

# 事件系统和可观察对象

CKEditor 5 具有基于事件的架构,因此您可以在任何地方找到 EmitterObservable 的混合。这两种机制都允许代码解耦并使其可扩展。

大多数已经提到的类要么是发射器,要么是可观察对象(可观察对象也是发射器)。发射器可以发射(触发)事件以及监听事件。

class MyPlugin extends Plugin {
    init() {
        // Make MyPlugin listen to someCommand#execute.
        this.listenTo( someCommand, 'execute', () => {
            console.log( 'someCommand was executed' );
        } );

        // Make MyPlugin listen to someOtherCommand#execute and block it.
        // You listen with a high priority to block the event before
        // someOtherCommand's execute() method is called.
        this.listenTo( someOtherCommand, 'execute', evt => {
            evt.stop();
        }, { priority: 'high' } );
    }

    // Inherited from Plugin:
    destroy() {
        // Removes all listeners added with this.listenTo();
        this.stopListening();
    }
}

第二个监听'execute'的监听器展示了 CKEditor 5 代码中常见的做法之一。基本上,'execute'的默认操作(即调用execute()方法)被注册为该事件的监听器,并具有默认优先级。因此,通过使用'low''high'优先级监听事件,您可以在execute()真正被调用之前或之后执行一些代码。如果您停止了事件,那么execute()方法将完全不会被调用。在本例中,Command#execute()方法使用ObservableMixin#decorate()函数用该事件进行了装饰。

import { ObservableMixin, mix } from 'ckeditor5';

class Command {
    constructor() {
        this.decorate( 'execute' );
    }

    // Will now fire the #execute event automatically.
    execute() {}
}

// Mix ObservableMixin into Command.
mix( Command, ObservableMixin );

查看事件系统深入指南可观察对象深入指南,了解有关使用事件和可观察对象的一些其他示例的更高级用法。

除了用事件装饰方法之外,可观察对象还允许观察其选定的属性。例如,Command类通过调用set()使它的#value#isEnabled可观察。

class Command {
    constructor() {
        this.set( 'value', undefined );
        this.set( 'isEnabled', undefined );
    }
}

mix( Command, ObservableMixin );

const command = new Command();

command.on( 'change:value', ( evt, propertyName, newValue, oldValue ) => {
    console.log(
        `${ propertyName } has changed from ${ oldValue } to ${ newValue }`
    );
} )

command.value = true; // -> 'value has changed from undefined to true'

可观察对象还有一个广泛用于编辑器(尤其是在 UI 库中)的功能——能够将一个对象属性的值绑定到其他属性的值(一个或多个对象)。当然,这也可以通过回调来处理。

假设targetsource是可观察对象,并且使用的属性是可观察的

target.bind( 'foo' ).to( source );

source.foo = 1;
target.foo; // -> 1

// Or:
target.bind( 'foo' ).to( source, 'bar' );

source.bar = 1;
target.foo; // -> 1

您还可以在UI 库架构指南中找到有关用户界面中数据绑定的更多信息。

在您了解了如何创建插件和命令之后,您可以阅读编辑引擎指南,了解如何实现真正的编辑功能。