CKEditor5编辑器介绍和架构角度分析

287 阅读10分钟

以下基于ckeditor@22.0.0。

〇、CKEditor编辑器介绍

一、架构角度

二、整体分析代码

三、ckeditor5-core

四、ckeditor5-engine

五、ckeditor5-ui

六、ckeditor-utils

〇、CKEditor编辑器介绍

详见ckeditor.com/docs/ckedit…

也可在官方提供的interactive builder网站上查看不同类型的编辑器。

1.编辑器类型

Classic editor

经典编辑器类型显示一个带有工具栏的框式编辑区域,工具栏位于页面的特定位置。”

Inline editor

内联编辑器类型允许你直接在目标位置创建内容,通过一个浮动的工具栏来帮助编辑,该工具栏在可编辑文本聚焦时出现。

image.png

Balloon editor

悬浮(balloon)编辑器类型允许你直接在目标位置创建内容,借助一个悬浮(balloon)工具栏,该工具栏在选中的可编辑文档元素旁边出现。

image.png

Balloon block editor

气球块编辑器类型允许您直接在目标位置创建内容,借助两个工具栏:

  1. 一个气球工具栏,出现在选中的可编辑文档元素旁边(提供内联内容格式化工具)。
  2. 一个块级工具栏,通过工具栏手柄按钮可访问,该按钮是附加在可编辑内容区域的拖动指示器,并随着文档中的选择而移动(提供额外的块级格式化工具)。拖动指示器按钮也是一个手柄,可用于拖放内容块。

image.png

Document editor

下面示例中的编辑器是一个功能丰富的preset,专注于类似于本地文字处理器的富文本编辑体验。它最适合用于创建通常会被打印或导出为 PDF 文件的文档。

可在DecoupledEditor 基础上创建这种类型的编辑器(及类似编辑器)并自定义 UI 布局。

image.png

Multi-root editor

多根编辑器类型是一种具有多个独立可编辑区域的编辑器类型。

使用多根编辑器与使用多个独立编辑器(如内联编辑器示例中所示)之间的主要区别在于,在多根编辑器中,所有可编辑区域都属于同一个编辑器实例,共享相同的配置、工具栏和撤销堆栈,并生成一个文档。

image.png

还有其他类型,可以参考interactive builder网站。

2.Classic editor详细介绍

2.1使用方式

方式1
// You can initialize the editor using an existing DOM element: 
ClassicEditor 
.create( document.querySelector( '#editor' ) ) 
.then( editor => {
  console.log( 'Editor was initialized', editor );
} )
.catch( err => {
  console.error( err.stack );
} );
方式2
// Alternatively, you can initialize the editor by passing the initial data directly as a string.
// In this case, the editor will render an element that must be inserted into the DOM:
ClassicEditor
  .create( '<p>Hello world!</p>' )
  .then( editor => {
    console.log( 'Editor was initialized', editor ); 
    // Initial data was provided so the editor UI element needs to be added manually to the DOM. 
    document.body.appendChild( editor.ui.element );   } )
  .catch( err => {
    console.error( err.stack );
  } );
方式3

混合了方式1和方式2

// You can also mix these two ways by providing a 
DOM element to be used and passing the initial data through the configuration: 
ClassicEditor
  .create( document.querySelector( '#editor' ),{ 
    initialData: '<h2>Initial data</h2><p>Foo bar.</p>'
  } )
  .then( editor => {
    console.log( 'Editor was initialized', editor );   } )
  .catch( err => {
    console.error( err.stack );
  } );

2.2源码分析

ClassicEditor类位于ckeditor5-build-classic,继承了ckeditor5-editor-classic中的ClassicEditor类。

// https://github.com/ckeditor/ckeditor5/blob/v22.0.0/packages/ckeditor5-editor-classic/src/classiceditor.js
import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';

export default class ClassicEditor extends Editor {
  constructor( sourceElementOrData, config ) {
    super( config );
    if ( isElement( sourceElementOrData ) ) { 
      this.sourceElement = sourceElementOrData;
    }
    
    this.data.processor = new HtmlDataProcessor( this.data.viewDocument );
    
    this.model.document.createRoot(); 
    
    const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' ); 
    const view = new ClassicEditorUIView( this.locale, this.editing.view, { shouldToolbarGroupWhenFull } ); 
    
    this.ui = new ClassicEditorUI( this, view );
    
    attachToForm( this );
  }
  destroy() {
    if ( this.sourceElement ) {
      this.updateSourceElement();
    }
    
    this.ui.destroy();
    
    return super.destroy();
  }
  
  static create( sourceElementOrData, config = {} ) {
    return new Promise( resolve => {
      const editor = new this( sourceElementOrData, config );
      
      resolve(
        editor.initPlugins()
          .then( () => editor.ui.init( isElement( sourceElementOrData ) ? sourceElementOrData : null ) )
          .then( () => {
            if ( !isElement( sourceElementOrData ) && config.initialData ) {
            // Documented in core/editor/editorconfig.jdoc. // eslint-disable-next-line ckeditor5-rules/ckeditor-error-message 
              throw new CKEditorError( 'editor-create-initial-data', null );
            }
            
            const initialData = config.initialData || getInitialData( sourceElementOrData );
            
            return editor.data.init( initialData ); 
         } )
         .then( () => editor.fire( 'ready' ) ) 
         .then( () => editor )
       );
     } );
   }
}

待后期详述。

一、架构角度(by chatgpt)

CKEditor 5 是一款功能强大的基于 JavaScript 的富文本编辑器,其设计和架构相较于前几代(如 CKEditor 4)做了重大改进。CKEditor 5 是基于现代 JavaScript 框架和模块化思想开发的,更具扩展性和可维护性。以下从架构角度详细介绍 CKEditor 5 的关键设计元素和模块:

1.模块化设计

CKEditor 5 采用模块化架构,整个编辑器被拆分为多个模块,每个模块负责一个独立的功能。这种设计使得开发者可以按需引入和扩展功能,灵活定制编辑器的行为。模块化设计基于 ES6 的模块规范,可以通过import和export来引用模块。

核心模块:CKEditor 5 的核心模块提供了基础的编辑功能,比如内容编辑、事件处理、插件管理等。

插件模块:大部分功能(如表格、图片、链接、公式等)都通过插件实现,插件的引入和扩展非常灵活。

引擎模块:CKEditor 5 中的@ckeditor/ckeditor5-engine模块是其核心的编辑引擎,负责文档模型、视图和编辑器的渲染。

2.MVC 架构

CKEditor 5 采用了类似于 MVC(Model-View-Controller)的设计模式,进一步增强了其架构的清晰性和可扩展性。

image.png

模型(Model):CKEditor 5 中的模型是数据的抽象表示,独立于任何具体的 DOM 结构。编辑器操作的数据都通过模型进行管理,保持了编辑内容的纯粹性,不会直接与 DOM 交互。这使得它对数据操作更具一致性和灵活性。

视图(View):视图是 CKEditor 5 中内容呈现的部分。视图层负责将模型的数据映射到实际的 DOM 结构中,并在用户操作时更新 DOM。视图是只读的,所有的内容修改都需要通过模型更新。

控制器(Controller):控制器在 CKEditor 5 中主要通过命令(Commands)来体现。命令负责管理用户操作和模型之间的交互,如用户输入文本、加粗、撤销等。每个命令都会修改模型,而不是直接操作视图。

3.数据模型(Data Model)

CKEditor 5 引入了强大的数据模型,这个模型是编辑内容的抽象表示,不与具体的 HTML 结构绑定。模型的数据结构类似于树状结构(类似 DOM 树),每个节点代表编辑内容中的某个元素(如段落、文本节点、加粗节点等)。这种设计带来了几个优势:

  • 抽象化的内容表示:模型与最终渲染的 HTML 无关,允许 CKEditor 5 进行复杂的格式和结构管理,如多文档格式支持(例如 Markdown 或 XML)。
  • 保持一致性:由于所有内容的变化首先体现在模型上,而模型变化又会反映到视图层,确保了编辑过程中的数据一致性。

4.视图模型(View Model)

视图模型(View Model)在 CKEditor 5 中负责将抽象的模型映射到具体的 HTML DOM 结构中。它通过虚拟 DOM 技术,确保高效的 DOM 操作,减少不必要的页面重绘和性能开销。

虚拟 DOM:CKEditor 5 使用虚拟 DOM 来管理视图的更新。虚拟 DOM 是对实际 DOM 的抽象,在每次更新时,它会计算出最小的修改操作来优化性能。

双向绑定:CKEditor 5 的视图与模型是双向绑定的,即视图的改变会更新模型,而模型的改变也会自动更新视图。这保证了内容在编辑器中的实时更新和同步。

5.事件驱动机制

CKEditor 5 内部大量使用了事件驱动的编程模型,使得它在扩展功能时更具灵活性。每个模块、插件以及编辑器实例都可以订阅和触发事件,从而实现组件之间的通信。

全局事件总线:CKEditor 5 提供了一个全局的事件系统,不同的模块可以通过该系统发布和监听事件。这使得各个插件或功能模块可以相互解耦,彼此独立地工作。

命令和监听器:CKEditor 5 中的命令系统基于事件来实现,编辑器中的大部分操作(如撤销、重做、格式化等)都可以通过命令触发,并且可以被监听和扩展。

6.插件系统

CKEditor 5 的插件系统是其架构中最灵活、最强大的部分之一。几乎所有功能都是通过插件来实现的,开发者可以选择性地引入或移除插件。插件之间通过依赖注入和事件机制进行通信和协作。

插件的生命周期:插件的生命周期分为初始化(init)、启动(afterInit)等多个阶段,开发者可以在这些阶段中注册逻辑来扩展或修改编辑器的行为。

插件的可扩展性:开发者可以编写自定义插件,通过继承已有插件或编写全新的功能模块,来扩展 CKEditor 5 的功能。此外,CKEditor 5 的核心功能也都是通过插件形式实现的,因此其功能非常灵活。

7.文档协作功能

CKEditor 5 提供了实时协作功能,支持多个用户同时编辑同一文档。这个功能是通过一个复杂的协作引擎来实现的,利用了操作变换(Operational Transformation, OT)和 CRDT(Conflict-free Replicated Data Types)等技术来确保多个用户同时编辑的内容冲突得到合理处理。

实时协作引擎:CKEditor 5 的协作引擎会跟踪每个用户的操作,并将它们同步到所有参与编辑的客户端。在这种架构中,编辑内容首先在模型层进行操作,然后通过网络同步给其他用户的编辑器实例,确保所有人看到的内容一致。

8.文档输出与输入

CKEditor 5 支持多种文档格式的输入与输出。由于其内容模型与 HTML 等具体格式解耦,因此可以方便地将编辑器内容转换为多种格式。

HTML 数据处理:通过@ckeditor/ckeditor5-html-support插件,CKEditor 5 可以解析和输出复杂的 HTML 结构。

Markdown 和其他格式:借助不同的插件,CKEditor 5 也可以支持将内容转换为 Markdown、JSON 等格式。

9.可定制性与配置

CKEditor 5 的可定制性非常强,几乎所有功能和行为都可以通过配置选项或扩展插件来调整。开发者可以基于现有的模块或插件,实现自定义的行为逻辑。

编辑器配置:通过传递不同的配置对象,可以控制 CKEditor 5 的默认行为,如工具栏、快捷键、内容过滤规则等。

主题与样式:CKEditor 5 提供了灵活的样式和主题机制,开发者可以通过修改 CSS 或自定义主题来更改编辑器的外观。

10.性能优化

CKEditor 5 在性能优化上做了大量工作,特别是在处理大型文档和复杂结构时。

延迟加载:通过模块化设计和按需加载,CKEditor 5 支持延迟加载部分插件或功能,减少初始加载时间。

批量操作:CKEditor 5 通过批处理机制将多个编辑操作组合在一起,减少不必要的视图更新和 DOM 操作。

CKEditor 5 的架构以模块化、MVC 和事件驱动为核心,充分考虑了现代 Web 开发中的可扩展性、可维护性和性能优化。其基于数据模型的设计使得它能够轻松支持多种文档格式,并且插件化的架构让开发者能够灵活地扩展和定制功能。这种设计使得 CKEditor 5 成为一款适合现代 Web 应用的高度可扩展的富文本编辑器。