核心软件包@ckeditor/ckeditor5-core相对简单,只包含少数几个类。

编辑器 Editor​

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

  • config - 配置对象;

  • pluginscommands(插件和指令)- 已加载插件和指令的集合;

  • model - 编辑器数据模型的入口点;

  • data - 数据控制器。它控制如何从文档中获取数据并在其中进行设置;

  • editing - 可编辑元件控制器。它控制如何将模型渲染给用户进行编辑;

  • keystrokes - 按键处理程序。它允许将按键与操作绑定。

除此之外,编辑器还暴露了一些方法:

  • create() - 静态create()方法。编辑器自身的构造函数是受保护的,因此应使用此静态方法创建编辑器。此方法允许异步初始化;

  • destroy() - 销毁编辑器;

  • execute() - 执行给定的命令;

  • setData()getData() - 从编辑器中获取数据,和在编辑器中设置数据的方法。数据格式由数据控制器的数据处理器控制;

有关方法的完整列表,请查阅所使用的 Editor 类的 API 文档。某些定制的编辑器可能会提供额外方法。

类 Editor 是自行定制编辑器的基础。CKEditor 5 框架原生附带了几种编辑器类型,但开发人员仍可以自由实现工作方式和外观完全不同的编辑器。

插件 Plugin​

插件是引入编辑器功能的一种方式。在 CKEditor 5 中,就连键入(Typing)功能都是一个插件。更重要的是,键入插件依赖于输入(Input)和删除(Delete)插件,这两个插件分别负责处理插入文本和删除内容的方法。又或者,某些插件需要在某些情况下自定义退格(Backspace)行为并自行处理。

用于实现 CKEditor 5 插件的的另一个重要策略是:将其分为引擎(Editing Engine)和 UI 两大部分。例如:对于加粗功能而言,BoldEditing 子插件负责引入模式定义、渲染<strong>标签的机制,以及应用和移除文本加粗的命令;而 BoldUI 子插件则添加了该功能的用户界面(即按钮)。这种功能分离旨在实现更高的重用性(可以获取引擎部分并为功能实现自己的 UI),以及支持在服务器端运行 CKEditor 5。最后使用一个空壳 Bold 插件来集成它们,即可提供完整的体验。

总结起来就是:

  1. 每个功能都是通过插件实现或启用的;

  2. 插件被高度细化;

  3. 插件构成编辑器的一切;

  4. 插件之间应尽可能少地耦合。

这些就是官方内置插件的实现标准。在开发者实现自己的插件时,如果不打算发布,则可以只保留此列表的第一点。

在冗长的介绍之后(目的是令读者更容易消化已有的内置插件),可以再来解释一下插件 API。

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

JavaScript
import MyDependency from 'some/other/plugin';
class MyPlugin extends Plugin {
    static get requires() {
        return [ MyDependency ];
    }

    init() {
        // 在此处初始化插件
        // 负责加载此插件的编辑器实例
        this.editor;
    }
}

指令 Command​

指令是执行动作(Execute)和状态(State)(一组属性)的组合。例如:粗体指令会应用或删除所选文本的粗体属性。如果选中的文本已经加粗,则该指令的值为 true,否则为false。如果粗体指令可以在当前选区中执行,则启用该指令。如果不能(比如因为此处不允许加粗)则会禁用。

建议使用官方提供的 CKEditor 5 检查器(Inspector)进行开发和调试,它会提供大量有关编辑器状态的有用信息,如内部数据结构、选区和指令等。

所有指令都应继承自Command类。指令需要添加到编辑器的指令集中,以便使用Editor::execute()方法执行。

JavaScript
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这样的典型指令的状态管理是如何实现的,请看一下作为bold基础的AttributeCommand类的内容。

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

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

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

当 Model 发生任何变化时,(指令本身)会自动调用此方法。这意味着当编辑器中有任何变化时,指令会自动刷新自己的状态。

指令的重要之处在于,其状态的每次变化以及调用execute()方法都会触发事件。例如当修改#value属性时,会触发#set:value#change:value事件;执行指令时则会触发#execute事件。通过这些事件,可以从外部控制指令。例如,如果想在某些条件为true时禁用特定的指令(例如根据某些定制的逻辑,这些指令应暂时不可用),而且没有其它更简洁的方法,则可以手动禁用指令:

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

    cmd.isEnabled = false;

    // 使再次启用指令成为可能
    return () => {
        cmd.off( 'set:isEnabled', forceDisable );
        cmd.refresh();
    };

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

// 用法:

// 禁用指令
const enableBold = disableCommand( editor.commands.get( 'bold' ) );

// 重新启用指令
enableBold();

现在只要不关闭该监听器,无论调用多少次someCommand.refresh(),该指令都会被禁用。默认情况下,当编辑器处于只读模式时,编辑器指令会被禁用。但是,如果指令不会修改编辑器数据,且希望其在只读模式下仍然能保持启用状态,则可以将affectsData标志设置为false

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

        // 即使编辑器为只读状态,该命令也将保持启用
        this.affectsData = false;
    }
}

在限制用户写入权限的其它编辑器模式下,affectsData标志也会影响指令。此外,所有编辑器指令的affectsData标志都默认设置为true,除非指令需要在编辑器只读模式下启用,否则无需对其做出更改。该标志位在编辑器生命周期内不可更改。

Event & Observable​

CKEditor 5 采用了基于事件的架构,因此EmitterObservable被混合在一起。这两种机制都允许代码解耦,且使其具有可扩展性。前面提到的大多数类都是EmitterObservableObservable也是Emitter)。Emitter可以触发事件,也可以监听事件。

JavaScript
class MyPlugin extends Plugin {
    init() {
        // 使 MyPlugin 监听 someCommand#execute
        this.listenTo( someCommand, 'execute', () => {
            console.log( 'someCommand 被执行。' );
        } );

        // 让 MyPlugin 监听 someOtherCommand#execute 并阻断它
        // 可使用高优先级监听,以便在调用 someOtherCommand 的 execute() 方法之前阻断该事件
        this.listenTo( someOtherCommand, 'execute', evt => {
            evt.stop();
        }, { priority: 'high' } );
    }

    // 继承自 Plugin:
    destroy() {
        // 移除所有通过 this.listenTo() 添加的监听器
        this.stopListening();
    }
}

execute的第二个监听器表征了 CKEditor 5 代码中的一种常见做法。一般地,execute的默认操作(即调用execute()方法)以默认优先级注册为该事件的监听器。因此,通过使用lowhigh优先级监听事件,便可以在真正调用execute()之前或之后执行某些代码。如果阻断事件,则根本不会调用execute()方法。在本例中,Command::execute()方法使用ObservableMixin::decorate()函数修饰了事件:

JavaScript
import { ObservableMixin, mix } from 'ckeditor5';

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

    // 现在将自动触发 #execute 事件
    execute() {}
}

// 将 ObservableMixin 混合到指令中
mix( Command, ObservableMixin );

除了用事件修饰方法以外,可观察对象还允许观察其所选属性。例如,Command类可通过调用set()来观察其#value#isEnabled

JavaScript
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 } 已从 ${ oldValue } 修改至 ${ newValue }。`
    );
} )

command.value = true; // -> '属性 value 已从 undefined 修改至 true。'

可观察对象还有一个被编辑器广泛使用的功能(尤其是在 UI 库中)——可以将一个对象的属性值与其它属性值绑定(一个或多个对象的属性)。当然也可以通过回调来处理。假设目标和源都是可观察对象,且使用的属性都是可观察属性:

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

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

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

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