整体分析代码和ckeditor5-core

232 阅读23分钟

以下基于ckeditor@22.0.0。

〇、CKEditor编辑器介绍

一、架构角度

二、整体分析代码

三、ckeditor5-core

四、ckeditor5-engine

五、ckeditor5-ui

六、ckeditor-utils

二、整体分析代码(by chatgpt)

CKEditor 5 的代码是基于现代 JavaScript(ES6+)设计的,采用模块化、面向对象、事件驱动等设计理念,架构上相当灵活。它不仅实现了强大的富文本编辑功能,还通过其良好的可扩展性、清晰的层次结构和高性能的设计赢得了广泛的用户基础。接下来,我将详细分析 CKEditor 5 的代码,从代码结构、核心组件、事件处理、插件系统、命令机制等几个方面入手。

1.代码结构与模块化

CKEditor 5 的代码被分成多个独立的包,每个包代表一个功能模块。这些模块都位于官方的npm仓库中,并且它们以独立的 ES6 模块发布。这些模块可以单独安装和使用,支持按需加载。

模块化架构

CKEditor 5 是完全模块化的,主要模块包括:

  • @ckeditor/ckeditor5-core: 提供编辑器的核心功能,包括插件管理、命令系统等。
  • @ckeditor/ckeditor5-engine: 编辑器引擎,处理文档模型、视图和渲染。
  • @ckeditor/ckeditor5-ui: 用户界面相关组件,包括工具栏、按钮、对话框等。
  • @ckeditor/ckeditor5-typing: 提供文本输入和基本的键盘交互支持。
  • @ckeditor/ckeditor5-paragraph: 支持段落操作。
  • @ckeditor/ckeditor5-basic-styles: 包含粗体、斜体等基本文本样式的插件。

这些模块可以通过import语法在项目中引入。例如,以下代码导入并初始化了一个基础的编辑器:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
ClassicEditor   
  .create(document.querySelector('#editor'), {
    plugins: [Paragraph, Bold, Italic],
    toolbar: ['bold', 'italic']
  })
  .then(editor => {
    console.log('Editor was initialized', editor);
  })
  .catch(error => {
    console.error('There was a problem initializing the editor.', error);
  });

2.文档模型与视图引擎

CKEditor 5 的文档模型和视图引擎是其代码架构的核心部分。这一设计的目的是将编辑内容的内部表示与用户看到的 HTML 结构解耦。通过这种方式,模型可以以一致的方式操作,而不依赖于特定的 DOM 结构。

模型层

模型层采用树状结构来表示文档内容,它独立于 HTML。它的结构类似于 DOM 树,但其中只包含抽象的文档元素。每个文档元素都有明确的语义和位置信息。

模型层的节点分为两大类:

块级元素:如段落、标题等,它们定义了文档的整体结构。

内联元素:如文本、链接、加粗等,它们定义了具体的内容和格式。

例如,创建一个简单的模型结构可能如下:

const model = editor.model;
const root = model.document.getRoot();
const position = model.createPositionAt(root, 'end');
model.change(writer => {
  writer.insertText('Hello, World!', { bold: true }, position);
});

这个例子展示了如何通过模型 API 向文档中插入加粗的文本。

视图层

视图层负责将模型层的数据映射到 DOM 结构,并处理用户的输入。CKEditor 5 采用了虚拟 DOM 技术来管理视图,确保在处理复杂的文档结构时有良好的性能。

视图和模型通过映射机制互相同步:

模型到视图映射:当模型变化时(如用户输入或命令执行),视图层会根据映射规则更新相应的 DOM。

视图到模型映射:当用户在编辑器中进行操作时,视图层会将用户的输入转换为相应的模型操作。

3.事件驱动与观察者模式

CKEditor 5 采用事件驱动机制,很多模块和插件都通过事件进行交互。CKEditor 5 中的每个对象都可以注册和触发事件。它通过EmitterMixin为对象添加了发布/订阅机制,使得插件、命令和编辑器核心能够很好地协同工作。

例如,监听内容变化事件:

editor.model.document.on('change:data', () => { 
  console.log('The content has changed!');
});

在这个例子中,当编辑器的内容发生变化时,change:data事件会被触发,开发者可以在此时进行一些自定义操作。

观察者模式

CKEditor 5 广泛使用了观察者模式,不同的模块可以监听特定对象的状态变化。例如,命令系统通过监听模型的变化来激活或禁用命令。

4.命令机制

CKEditor 5 中的所有用户操作(如加粗、斜体、插入图片等)都是通过命令系统来实现的。命令系统是编辑器的核心控制机制,所有操作都通过命令执行,并在执行过程中修改文档模型。

命令的基本结构

每个命令继承自Command类,并实现execute方法,该方法包含命令的具体操作逻辑。例如,加粗命令的实现可能如下:

class BoldCommand extends Command {
  execute() {
    this.editor.model.change(writer => {
      const selection = this.editor.model.document.selection; 
      writer.setSelectionAttribute('bold', true); 
    });
  }
  
  refresh() {
    const selection = this.editor.model.document.selection;
    this.isEnabled = selection.hasAttribute('bold');
  }
}

在这个例子中,execute方法修改了模型中的选中内容,refresh方法则控制命令是否可用(即用户选择的文本是否已经加粗)。

注册命令

每个插件都可以通过editor.commands.add方法注册一个新命令。注册后的命令可以通过编辑器实例调用,或通过键盘快捷键触发。

editor.commands.add('bold', new BoldCommand(editor));

5.插件系统

插件系统是 CKEditor 5 的核心扩展机制。几乎所有的功能(如段落、加粗、插入图片等)都是通过插件提供的。插件不仅可以增加新功能,还可以扩展现有功能。

插件的基本结构

一个插件通常是一个 JavaScript 类,它需要实现init()方法。在编辑器初始化时,插件的init()方法会被调用。一个简单的插件如下:

class MyCustomPlugin extends Plugin {
  init() {
    console.log('MyCustomPlugin was initialized!'); 
    const editor = this.editor;
    // 自定义功能逻辑
  }
}

插件之间的依赖

插件可以相互依赖,某些插件可能依赖于其他插件提供的功能。在插件类中,可以通过static get requires()明确依赖关系:

class MyAdvancedPlugin extends Plugin {
  static get requires() {
    return [Paragraph, Bold];
  }
}

在这个例子中,MyAdvancedPlugin依赖Paragraph和Bold插件。

插件的生命周期

插件有一个明确的生命周期,通常包括以下几个阶段:

  • constructor:插件类的构造函数,初始化插件的基本属性。
  • init():插件的初始化逻辑,通常在编辑器创建时调用。
  • afterInit():所有插件都已初始化之后调用,适合处理跨插件的交互。
  • destroy():插件销毁时调用,清理资源或解除事件监听。

6.数据处理与转换

CKEditor 5 支持多种数据格式的输入与输出,例如 HTML、Markdown、JSON 等。这是通过DataProcessor接口来实现的,不同的数据格式处理器实现了该接口。

HTML 处理器

默认情况下,CKEditor 5 使用 HTML 作为内容的输入和输出格式。HTML 处理器通过解析和序列化模型,支持复杂的 HTML 结构。

editor.setData('<p>This is a <strong>test</strong></p>');
const data = editor.getData();
console.log(data);
// <p>This is a <strong>test</strong></p>

自定义数据处理器

开发者也可以编写自定义的数据处理器,将内容输出为其他格式,如 Markdown。

CKEditor 5 的代码架构基于模块化、面向对象和事件驱动的设计原则。它通过 MVC 模型和虚拟 DOM 技术,确保了编辑器的高性能和灵活性。通过命令机制和插件系统,CKEditor 5 提供了极高的可扩展性,几乎所有的功能都可以通过插件来实现和扩展。该架构既适合简单的文本编辑需求,也能够扩展为复杂的文档协作和内容管理系统。

三、ckeditor5-core

@ckeditor/ckeditor5-core是 CKEditor 5 的核心模块,它定义了编辑器的基础架构和核心功能。在版本22.0.0中,作为 CKEditor 5 生态系统中的关键组件,它的主要职责包括提供插件管理、编辑器初始化、事件处理等核心机制。接下来,我将详细分析22.0.0版本的一些关键点和功能。

0.编辑器类

编辑器类是整个 CKEditor 5 的核心,它集成了插件管理、事件处理、数据操作等功能。在编辑器初始化时,插件会通过 Editor 进行注册,并触发插件的初始化方法。

// @ckeditor/ckeditor5-core/src/editor/editor.js 
import Context from '../context';
import PluginCollection from '../plugincollection';
import CommandCollection from '../commandcollection';
import EditingKeystrokeHandler from '../editingkeystrokehandler';
import EditingController from '@ckeditor/ckeditor5-engine/src/controller/editingcontroller';
import DataController from '@ckeditor/ckeditor5-engine/src/controller/datacontroller';
import Conversion from '@ckeditor/ckeditor5-engine/src/conversion/conversion';
import Model from '@ckeditor/ckeditor5-engine/src/model/model';
import { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; 
import Config from '@ckeditor/ckeditor5-utils/src/config';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
export default class Editor {
  constructor(config = {}) {
    // defaultConfig是内置的插件,eg:
    // ClassicEditor.builtinPlugins = [ FooPlugin, BarPlugin ];
    const availablePlugins = Array.from( this.constructor.builtinPlugins || [] );
    
    this.config = new Config( config, this.constructor.defaultConfig ); 
    this.config.define( 'plugins', availablePlugins );
    this.config.define( this._context._getEditorConfig() );
    /**
    * 创建插件集合
    * The plugins loaded and in use by this editor instance.
    * editor.plugins.get( 'Clipboard' ); // -> An instance of the clipboard plugin.
    */
    this.plugins = new PluginCollection( this, availablePlugins, this._context.plugins );
    /**
    * Commands registered to the editor.
    * Use the shorthand editor.execute() method to execute commands:
    * // Execute the bold command:
    * editor.execute( 'bold' );
    *
    * // Check the state of the bold command:
    * editor.commands.get( 'bold' ).value;
    */
    this.commands = new CommandCollection();
    /**
    * 通过mix( Editor, ObservableMixin )引入ObservableMixin能力
    */
    this.set( 'state', 'initializing' );
    this.once( 'ready', () => ( this.state = 'ready' ), { priority: 'high' } );
    this.once( 'destroy', () => ( this.state = 'destroyed' ), { priority: 'high' } );
    
    this.set( 'isReadOnly', false );
    /** * The editor's model.
    * Model类:ckeditor5-engine/src/model/model
    */
    this.model = new Model();
    const stylesProcessor = new StylesProcessor();
    /**
    * Used e.g. for setting and retrieving the editor data.
    */
    this.data = new DataController( this.model, stylesProcessor );
    /**
    * Controls user input and rendering the content for editing.
    */
    this.editing = new EditingController( this.model, stylesProcessor ); 
    this.editing.view.document.bind( 'isReadOnly' ).to( this );
    
    this.conversion = new Conversion( [ this.editing.downcastDispatcher, this.data.downcastDispatcher ], this.data.upcastDispatcher ); 
    this.conversion.addAlias( 'dataDowncast', this.data.downcastDispatcher ); 
    this.conversion.addAlias( 'editingDowncast', this.editing.downcastDispatcher );
    /**
    * An instance of the EditingKeystrokeHandler.
    * EditingKeystrokeHandler继承了KeystrokeHandler类(实现了对dom元素的事件监听)
    * It allows setting simple keystrokes:
    * // Execute the bold command on Ctrl+E:
    * editor.keystrokes.set( 'Ctrl+E', 'bold' );
    *
    * // Execute your own callback:
    * editor.keystrokes.set( 'Ctrl+E', ( data, cancel ) => {
    * console.log( data.keyCode );
    * // Prevent the default (native) action and stop the underlying keydown event
    * // so no other editor feature will interfere. 
    * cancel();
    * } );
    */
    this.keystrokes = new EditingKeystrokeHandler( this );
    this.keystrokes.listenTo( this.editing.view.document ); }
    // 初始化插件
    initPlugins() {
      const config = this.config;
      const plugins = config.get( 'plugins' );
      const removePlugins = config.get( 'removePlugins' ) || [];
      const extraPlugins = config.get( 'extraPlugins' ) || [];
      // this.plugins是PluginCollection实例
      return this.plugins.init( plugins.concat( extraPlugins ), removePlugins );
    }
    
    // 销毁编辑器
    destroy() {
      let readyPromise = Promise.resolve();
      if ( this.state == 'initializing' ) { 
        readyPromise = new Promise( resolve => this.once( 'ready', resolve ) );
      }
      
      return readyPromise .then(() => {
        this.fire( 'destroy' ); 
        this.stopListening(); 
        this.commands.destroy();
      } )
      .then( () => this.plugins.destroy() )
      .then( () => {
      // 销毁model、data、editing和keystrokes 
      this.model.destroy();
      this.data.destroy();
      this.editing.destroy(); 
      this.keystrokes.destroy();
    } )
    .then( () => this._context._removeEditor( this ) );
  }
  
  execute( ...args ) {
    return this.commands.execute( ...args );
  }
}

// 混入ObservableMixin的能力 mix( Editor, ObservableMixin );

Editor类构造函数,主要负责:

  • 实例化PluginCollection类,提供插件相关操作;
  • 实例化CommandCollection类,提供命令的保存、新增、执行、遍历和销毁等操作;
  • 实例化Model、StylesProcessor、DataController、EditingController、Conversion、EditingKeystrokeHandler(继承自KeystrokeHandler),详见ckeditor5-engine篇;
  • 设置isReadOnly、state值和ready、destroy事件的回调方法。

initPlugins方法,主要负责实例化插件,见“插件的依赖管理”篇。

destroy方法,主要负责销毁内存;

execute方法,提供命令执行方法。

1.插件机制

CKEditor 5 是一个模块化的编辑器,@ckeditor/ckeditor5-core的核心工作之一是管理插件机制。通过插件机制,开发者可以扩展编辑器的功能。

Plugin类是插件的基类,开发者通过继承它来创建自定义插件。每个插件都可以注册自己的功能、命令、UI 组件等。

插件的加载是通过Editor类完成的,核心模块负责插件的生命周期,包括插件的初始化、销毁等。

1.1 插件架构概述

CKEditor 5 使用插件的方式进行扩展。插件本质上是功能模块,可以执行特定的任务,如修改编辑器的行为、UI 组件、数据操作等。

CKEditor 5 的插件机制包括以下关键部分:

  • 插件基类(Plugin):所有插件都必须继承自 Plugin 类,该类提供了插件的生命周期管理和基础功能。
  • 命令(Command):插件通过命令来操作编辑器,执行具体的操作(如插入文本、改变样式等)。
  • 事件机制:编辑器及其插件使用事件来通知和响应状态变化。
  • UI 组件:插件可以添加新的 UI 元素,如按钮、菜单等,来提供用户交互功能。
  • 依赖管理:插件之间可以相互依赖,CKEditor 5 会自动管理插件的加载顺序。

1.2 Plugin 基类

所有插件必须继承自 Plugin 类,这是插件系统的核心。Plugin 类提供了生命周期方法、插件启用与禁用的机制等功能。插件的核心方法包括:

  • init():插件的初始化方法。通常,插件会在此方法中注册命令、事件监听器等功能。
  • destroy():插件销毁时调用,用于清理资源、移除事件监听器等。
  • isEnabled():检查插件是否启用。
  • enable():启用插件。
  • disable():禁用插件。

Plugin 基类代码(简化版)

// @ckeditor/ckeditor5-core/src/plugin.js
class Plugin {
  constructor( editor ) {
    /**
    * The editor instance.
    */
    this.editor = editor;
    this._disableStack = new Set(); 
  }
  
  forceDisabled( id ) {
    this._disableStack.add( id );
    if ( this._disableStack.size == 1 ) {
      this.on( 'set:isEnabled', forceDisable, { 
        priority: 'highest'
      });
      this.isEnabled = false;
    }
  }
  
  clearForceDisabled( id ) { 
    this._disableStack.delete( id );
    if ( this._disableStack.size == 0 ) {
      this.off( 'set:isEnabled', forceDisable ); 
      this.isEnabled = true;
    }
  }
  
  destroy() {
    this.stopListening();
  }
  
  static get isContextPlugin() {
    return false;
  }
}

mix( Plugin, ObservableMixin );

1.3 插件的生命周期

插件的生命周期是由 init() 和 destroy() 两个方法控制的。在插件的 init() 方法中,插件可以进行初始化操作,如注册命令、绑定事件等。destroy() 方法在插件销毁时调用,用于清理资源,例如移除事件监听器或清除定时器等。

插件生命周期代码示例

import { Plugin } from '@ckeditor/ckeditor5-core'; 
class MyPlugin extends Plugin {
  static get pluginName() {
    // 这里可以设置插件名称
    return 'MyPlugin';
  }
  
  static get requires() {
    // 这里可以设置依赖的组件
    return [ ...... ];
  }
  
  init() {
    console.log('MyPlugin is initialized');
    // 这里可以进行命令注册、事件绑定等操作
  }
  
  afterinit() {
    console.log('MyPlugin is after initialized');
  } 
  
  destroy() {
    console.log('MyPlugin is destroyed');
    // 这里可以进行资源清理、移除事件监听器等
  }
}

1.4 插件与命令(Commands)

插件的一个重要功能是定义和执行命令。命令是编辑器内部的操作单位,通常用于响应用户输入并修改编辑器的内容。命令通过 execute() 方法实现具体的操作。

Command 类

CKEditor 5 中的命令继承自 Command 基类,命令通过 execute() 方法来进行操作。命令的生命周期方法包括 isEnabled()(判断命令是否可执行)和 execute()(执行命令的核心方法)。

插件注册命令

插件通过 editor.commands.add() 注册命令。例如,如果你想创建一个插入文本的命令:

import { Plugin, Command } from '@ckeditor/ckeditor5-core';
class InsertTextCommand extends Command { 
  execute(text) {
    const model = this.editor.model;
    const doc = model.document;
    model.change(writer => {
      // 获取当前选区并插入文本
      const selection = doc.selection; 
      writer.insertText(text, selection.getFirstRange().end);
    });
  }
}

class MyPlugin extends Plugin {
  init() {
    // 将命令添加到编辑器 
    this.editor.commands.add('insertText', new InsertTextCommand(this.editor));
  }
}

1.5 插件与事件系统

CKEditor 5 使用事件驱动架构。插件可以监听和触发事件,事件用于在编辑器内部不同组件之间进行通信。例如,模型的变化会触发 change 事件,视图的更新会触发 render 事件等。

插件监听事件示例
import { Plugin } from '@ckeditor/ckeditor5-core';
class MyPlugin extends Plugin {
  init() {
    // 监听文档的变化事件 
    this.editor.model.document.on('change', () => { 
      console.log('Document changed!');
    });
  }
}

1.6 插件与 UI 组件

CKEditor 5 提供了 UI 组件(如按钮、下拉菜单、弹出窗口等),插件可以通过 UI API 来创建和管理这些组件。插件通过 editor.ui.componentFactory.add() 方法添加新的 UI 元素,如按钮和下拉框。

ButtonView 示例
import { Plugin } from '@ckeditor/ckeditor5-core'; 
import { ButtonView } from '@ckeditor/ckeditor5-ui'; 
class MyPlugin extends Plugin {
  init() { const editor = this.editor;
  // 创建并添加一个按钮 
  editor.ui.componentFactory.add('myButton', locale => {
    const button = new ButtonView(locale); 
    button.set({ label: 'My Button', icon: 'icon.png', tooltip: true });
    button.on('execute', () => {
      // 点击按钮时执行的操作 
      editor.model.change(writer => {
        const selection = editor.model.document.selection; 
        writer.insertText('Hello from My Plugin!', selection.getFirstRange().end);
      });
    });
    return button;
    });
  }
}

1.7 插件的依赖管理

CKEditor 5 插件的加载是按照依赖关系进行的。如果插件 A 依赖插件 B,CKEditor 5 会确保 B 在 A 之前加载和初始化。插件的依赖关系由 pluginCollection 管理,CKEditor 会根据插件的依赖顺序自动加载和初始化插件。

插件集合是管理所有插件的地方,它通常由 Editor 类的 plugins 属性提供。

// @ckeditor/ckeditor5-core/src/plugincollection.js 
class PluginCollection {
    constructor( context, availablePlugins = [], contextPlugins = [] ) {
        this._context = context;
        this._plugins = new Map();
        
        this._availablePlugins = new Map();
        for ( const PluginConstructor of availablePlugins ) {
            if ( PluginConstructor.pluginName ) { 
                this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
            }
        }
        this._contextPlugins = new Map();
        for ( const [ PluginConstructor, pluginInstance ] of contextPlugins ) {
            this._contextPlugins.set( PluginConstructor, pluginInstance );
            this._contextPlugins.set( pluginInstance, PluginConstructor );
            // To make it possible to require a plugin by its name.
            if ( PluginConstructor.pluginName ) { 
                this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
            }
        }
    }
    
    get( key ) {
        const plugin = this._plugins.get( key ); 
        return plugin;
    }
    
    has( key ) {
        return this._plugins.has( key );
    }
    
    init( plugins, removePlugins = [] ) {
        const that = this;
        const loading = new Set();
        const loaded = [];
        // 保存成功加载的插件
        const pluginConstructors = mapToAvailableConstructors( plugins );
        const removePluginConstructors = mapToAvailableConstructors( removePlugins );
        // loadPlugin方法会依次实例化插件
        return Promise.all( pluginConstructors.map( loadPlugin ) )
            .then( () => initPlugins( loaded, 'init' ) ) 
            .then( () => initPlugins( loaded, 'afterInit' ) )
            .then( () => loaded );
            
            function loadPlugin( PluginConstructor ) {
                return instantiatePlugin( PluginConstructor )
            }
            function instantiatePlugin( PluginConstructor ) {
                return new Promise( resolve => {
                    loading.add( PluginConstructor );
                    if ( PluginConstructor.requires ) {
                        PluginConstructor.requires.forEach( RequiredPluginConstructorOrName => {
                        
                        const RequiredPluginConstructor = getPluginConstructor( RequiredPluginConstructorOrName );
                        // 递归的方式加载插件的前置插件 
                        loadPlugin( RequiredPluginConstructor );
                    }
                );
            }
            // 插件实例化
            const plugin = that._contextPlugins.get( PluginConstructor ) || new PluginConstructor( context );
            loaded.push( plugin );
            resolve();
        }
            
        destroy() {
            const promises = [];
            for ( const [ , pluginInstance ] of this ) {
                if ( typeof pluginInstance.destroy == 'function' && !this._contextPlugins.has( pluginInstance ) ) {
                    promises.push( pluginInstance.destroy() );
                }
             }

             return Promise.all( promises );
         }
     }

PluginCollection 是 Editor 类的一个重要部分,它负责:

  • 获取插件(get()):通过插件类名来获取插件。
  • 销毁插件(destroy()):通过promise依次调用每个插件实例的destroy方法。
  • 初始化(init()):遍历插件,先实例化前置依赖的插件,然后进行实例化,再依次执行实例的init和afterinit方法,最后返回所有加载完的插件(loaded变量)。

2.编辑器架构

CKEditor 5 的架构基于Model-View-Controller(MVC) 模式,其中@ckeditor/ckeditor5-core负责协调模型 (Model) 和视图 (View) 之间的通信和同步。

Model:这是文档的语义表示。它维护文档内容的结构化形式,并使用树结构来存储和操作数据。Model层确保编辑器的内容和操作是语义正确的。

View:负责显示用户可以交互的内容。视图层基于 DOM(文档对象模型),提供用户界面上的操作功能。

Controller:控制器是@ckeditor/ckeditor5-core的核心,负责将用户的操作从视图层转发到模型层,并确保模型的更改被正确反映回视图层。

这种架构的优势在于:

分离关注点:模型和视图的分离使得开发者能够更容易地处理编辑器的内容和外观,而无需担心彼此的实现细节。

实时协作:CKEditor 5 通过这种架构可以更好地实现实时协作功能,不同的用户操作可以快速同步到同一个模型上。

3.事件系统

@ckeditor/ckeditor5-core提供了一个健壮的事件系统,用于在插件和编辑器之间通信。每个插件可以监听特定的事件,来响应用户交互或其他插件的操作。

// @ckeditor/ckeditor5-core/src/editor/editor.js
mix( Editor, ObservableMixin );

事件系统基于ObservableMixin,允许对象在事件触发时相互通信,这使得插件间的耦合度非常低,详见ckeditor5-utils中的EmitterMixin篇和ObservableMixin篇。

4.命令机制

命令是编辑器的核心交互方式,它允许执行特定的操作,如文本加粗、斜体、插入图片等。@ckeditor/ckeditor5-core提供了一个灵活的命令注册和执行机制。

每个命令都通过Command类来实现,插件可以扩展此类来定义自己的命令逻辑。命令不仅可以通过用户界面(按钮、快捷键等)触发,也可以通过 API 调用。

Command 类代码(简化版)

// @ckeditor/ckeditor5-engine/src/command/command.js 
export default class Command {
    constructor(editor) {
        this.editor = editor;
        this.set( 'value', undefined );
        this.set( 'isEnabled', false ); 
        this.decorate( 'execute' );
        this.on( 'execute', evt => {
            if ( !this.isEnabled ) {
                evt.stop();
            }
        }, { priority: 'high' } );
    }
    
    execute(...args) {
        // 子类实现具体的命令操作
    }
    
    isEnabled() {
        // 返回命令是否可以执行的状态
        return true;
    }
        
    refresh() {
        this.isEnabled = true;
    }
}

说明:

  • value用于定义命令的值。一个具体的命令类应该定义它所代表的含义。例如,'bold' 命令的值表示选区是否包含加粗的文本。也有可能不用定义值,例如无状态组件imageUpload。
  • isEnabled用于标识命令是否disabled。
  • execute:命令的execute方法执行后,如果发现isEnabled标识为false,那么会停止该事件(见第8~10行代码)。

5.数据转换

CKEditor 5 的数据模型与 DOM 结构并不是一一对应的,因此@ckeditor/ckeditor5-core负责管理数据的转换。在编辑器初始化或用户操作时,数据需要在模型和视图之间进行转换。

核心模块定义了一套转换器,将用户在视图层的操作转换为对模型的操作,并最终将模型的更改反映在视图上。这一机制确保了编辑器内容的一致性,特别是在处理复杂的文档结构时。

6.编辑器生命周期管理

@ckeditor/ckeditor5-core还负责编辑器实例的生命周期管理。它包括从编辑器初始化、配置插件、绑定事件、到编辑器销毁等一系列操作。

编辑器的创建过程是通过Editor类实现的,开发者可以根据需求配置编辑器的功能和插件。编辑器关闭或销毁时,核心模块会确保所有插件和事件都被正确释放,避免内存泄漏。

7.键盘支持与焦点管理

@ckeditor/ckeditor5-core为编辑器提供了键盘操作支持,允许开发者为插件或命令定义快捷键。它通过KeystrokeHandler类实现键盘事件的处理。

另外,核心模块负责管理编辑器的焦点状态,确保在不同的交互场景下,编辑器可以正确处理焦点获取与丢失的逻辑。

8.模块化与扩展性

CKEditor 5 的一个显著特征就是它的模块化设计,@ckeditor/ckeditor5-core是编辑器的基础模块,但并不包含具体的功能(如富文本编辑功能)。这些功能通过其他插件提供,如@ckeditor/ckeditor5-basic-styles提供基础样式功能。

核心模块提供了丰富的扩展点,使得开发者可以轻松地添加自定义功能或改变现有功能的行为。

9.Context

context.js 文件定义了 Context 类,这个类的目的是为 CKEditor 5 提供一个共享的上下文环境,允许多个编辑器实例共享某些配置、插件和服务。这对于需要创建多个编辑器实例并且希望它们共享一些功能或服务的场景非常有用。

9.1 Context 类的作用

Context 类是 CKEditor 5 中用于管理和共享编辑器实例之间的公共资源和配置的机制。它允许多个编辑器实例共享某些资源(如插件、数据处理服务),从而减少重复的配置和提高资源利用率。这对于需要同时管理多个编辑器或插件的场景非常有用。

9.2 主要功能

1. 共享服务和插件

Context 允许不同的编辑器实例共享某些服务和插件,避免每个实例都单独初始化这些服务。这可以显著提高性能,尤其是在创建多个编辑器实例时。

2. 集中管理配置:

如果有一些全局的配置项可以应用到多个编辑器实例上,Context 可以集中管理这些配置,从而减少配置重复并确保一致性。

3. 插件加载的优化

Context 还可以优化插件的加载过程,多个编辑器可以共享相同的插件集合,而不需要为每个编辑器实例单独加载插件。

4. 资源的生命周期管理

Context 负责管理共享资源的生命周期。例如,如果多个编辑器共享同一个 Context,当这个 Context 被销毁时,它会确保所有共享的资源也会被正确清理。

9.3 使用场景

1. 共享插件与服务

如果你需要在页面上创建多个编辑器实例,Context 类允许这些实例共享某些插件和服务。例如,某些插件可能只需要在上下文中注册一次,然后所有编辑器都可以使用这些插件,而无需每个编辑器单独注册。

import { Context } from '@ckeditor/ckeditor5-core';
// 创建一个 Context 实例
const context = new Context({ plugins: [ SomeSharedPlugin ] });

// 创建多个编辑器实例,共享同一个 Context 
ClassicEditor.create( element1, { context } ); 
ClassicEditor.create( element2, { context } );
2. 集中管理配置

Context 类还可以集中管理一些全局的配置选项。例如,如果你想为多个编辑器定义相同的语言、格式或其他全局配置,使用 Context 可以避免每个编辑器实例都重复定义这些配置。

3. 优化资源管理

在需要多个编辑器实例的场景中,重复加载和初始化插件和服务可能会导致性能下降。通过 Context,这些资源可以在编辑器之间共享,从而提高性能并减少资源浪费。

9.4关键方法
1. constructor( config )

Context 的构造函数,用于初始化一个新的 Context 实例。参数 config 是上下文的配置对象,其中可以包含插件和其他共享资源。

const context = new Context( { plugins: [ PluginA, PluginB ] } );
2. destroy()

销毁 Context 实例并清理它管理的所有共享资源。这确保了当 Context 不再被使用时,它不会占用内存或保持任何未释放的资源。

// 使用方法
context.destroy();

// destroy源码
destroy() {
    return Promise.all( Array.from( this.editors, editor => editor.destroy()) )
    .then( () => this.plugins.destroy() );
}
9.5 Context 与 Editor 的关系
  • 编辑器实例:编辑器实例是 CKEditor 的核心功能,用户直接与之交互。每个编辑器实例都可以独立运行,并拥有自己的配置、插件和服务。
  • Context 的共享:当创建多个编辑器实例时,可以通过 Context 来共享它们之间的某些资源或配置。这样做的好处是可以避免重复加载和初始化相同的插件、服务,提升整体性能和资源利用率。

编辑器可以通过 Context 来共享插件、配置等资源,但仍然保持各自的独立性。每个编辑器可以有自己的配置和行为,只是在某些资源上共享。

9.6 示例

假设你需要在同一页面上创建多个编辑器,并且希望它们共享某些全局配置(如语言设置)和插件。你可以使用 Context 来管理这些共享资源:

import { Context } from '@ckeditor/ckeditor5-core'; 
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/ClassicEditor';
import MyCustomPlugin from './mycustomplugin';
// 创建 Context 实例,定义全局插件和配置
const context = new Context( { plugins: [ MyCustomPlugin ], language: 'fr' } );

// 使用相同的 Context 创建多个编辑器实例 ClassicEditor.create( document.querySelector( '#editor1' ), { context } )
  .then( editor => {
    console.log( 'Editor 1 was initialized', editor );
  })
  .catch( error => {
    console.error( error );
  }
);
ClassicEditor.create( document.querySelector( '#editor2' ), { context } )
  .then( editor => {
    console.log( 'Editor 2 was initialized', editor );
  })
  .catch( error => {
    console.error( error );
  } );
  // 销毁 Context,当页面不再需要时
  context.destroy();
// Creates and initializes a new context instance.
const commonConfig = { ... };// Configuration for all the plugins and editors.
const editorPlugins = [ ... ];// Regular plugins here. 
Context
.create( {
    // Only context plugins here.
    plugins: [ ... ],
    // Configure the language for all the editors (it cannot be overwritten).
    language: { ... },
    // Configuration for context plugins.
    comments: { ... },
    ...
    // Default configuration for editor plugins.
    toolbar: { ... },
    image: { ... },
    ... 
} )
.then( context => {
    const promises = [];
    promises.push( ClassicEditor.create( 
        document.getElementById( 'editor1' ),
        { editorPlugins, context } ) );
        
    promises.push( ClassicEditor.create( 
        document.getElementById( 'editor2' ),
        {
            editorPlugins,
            context,
            toolbar: { ... }
         }
     ) );
     return Promise.all( promises );
 } );
// 通过builtinPlugins设置内置插件
Context
    .create()
    .then( context => {
        context.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
        context.plugins.get( BarPlugin ); // -> An instance of the Bar plugin. } );
// 通过defaultConfig设置默认配置
Context.defaultConfig = { foo: 1, bar: 2 };
Context
    .create()
    .then( context => {
        context.config.get( 'foo' ); // -> 1 
        context.config.get( 'bar' ); // -> 2
    } );
    // The default options can be overridden by the configuration passed to create().
    Context
        .create( { bar: 3 } )
        .then( context => {
            context.config.get( 'foo' ); // -> 1 
            context.config.get( 'bar' ); // -> 3
        }
    );

总结

  • Context 类的作用:Context 类用于在多个 CKEditor 5 实例之间共享插件、配置和服务。它通过共享资源来提高性能,避免重复加载和初始化相同的插件或服务。
  • 主要功能:包括共享插件、集中管理配置、优化资源使用以及管理共享资源的生命周期。
  • 使用场景:主要在需要创建多个编辑器实例时使用,特别是需要优化性能并减少配置重复的场景。

10.EditorUI

EditorUI是一个提供成功启动任何编辑器 UI 所需的最小接口的类。例如ClassEditor中用到的ClassEditorUI就是继承了EditorUI类。

11.其他

Editor类中用Config类对配置进行封装,该类在ckditor5-utils中的Config篇详解。