ckeditor编辑器源码阅读(一)

611 阅读3分钟

1. 编辑器介绍

ckeditor4

CKEditor 1-4可以代表传统编辑器的技术路线(同类型技术的主要是UEditor),主要依赖于浏览器原生的编辑能力,用户内容的输入是浏览器直接处理;加粗、斜体、回车等这类的处理则是捕获浏览器的事件来覆盖浏览器默认行为来实现,再辅以一些DOM的嵌套规则(dtd)和复杂数据输入(如粘贴)的过滤规则来约束数据的正确性,这类编辑器整体思路还是比较清晰的。

内容的可编辑主要依赖DOM的contentEditable属性,基于原生execCommand或者自定义扩展的execCommand去操作DOM实现富文内容的修改。

特点: 写入存基于dom的输入操作 , 特殊样式通过拦截拦截修改浏览器默认行为

缺点: 不同平台显示可能不一致, 很难协同操作, 因为是基于dom操作, 操作容易不可预测

优点: 输入不经过js处理, 输入会比较丝滑

对比Quill富文本编辑器,底层设计上 ckeditor4 虽然封装了 dom\event\plugins 等多抽象模块,但实际还是在直接对 dom 进行操作。而 Quill 封装了自己的 Delta 数据层(可以对比理解为 vue 的虚拟 dom一样的数据层), 在后续对外暴露操作数据层的相应 api , 使得操作相比于 ckeditor4 更可控可预测。ckeditor4 在兼容性方面对老浏览器可能更加友好~

基于以上问题, ckeditor5 直接全部重构了。

2. ckeditor4 执行流程

项目地址:Fork的项目

  1. 入口: /ckeditor.js

  2. /ckeditor.js 引入 core/ckeditor_base.js: window挂载 window.CKEDITOR

    // 下面是压缩了 core/ckeditor_base.js 的内容
    // Compressed version of core/ckeditor_base.js. See original for instructions.
    /* jshint ignore:start */
    /* jscs:disable */
    // replace_start
    window.CKEDITOR||(window.CKEDITOR=function()...
    
  3. /ckeditor.js 加载 core/loader.js:用于载入所有 core/xxx.js 模块

    // # ckditor.js
    // ckditor.js 底下了 加载loader.js, 同时注册了 _autoLoad = 'ckeditor', 后续这个模块会作为模块加载入口被自动加载
    if ( CKEDITOR.loader )
        CKEDITOR.loader.load( 'ckeditor' );
    else {
        // Set the script name to be loaded by the loader.
        CKEDITOR._autoLoad = 'ckeditor';
    ​
        // Include the loader script.
        if ( document.body && ( !document.readyState || document.readyState == 'complete' ) ) {
            var script = document.createElement( 'script' );
            script.type = 'text/javascript';
            script.src = CKEDITOR.getUrl( 'core/loader.js' );
            document.body.appendChild( script );
        } else {
            document.write( '<script type="text/javascript" src="' + CKEDITOR.getUrl( 'core/loader.js' ) + '"></script>' );
        }
    }
    
    // # loader.js 
    // loader.js 向 CKEDITOR 上插入 loader 对象, 暴露 load 方法,后续通过 向 html 直接注入 script 标签进行引入模块
    document.write( '<script src="' + scriptSrc + '" type="text/javascript"></script>' );
    ​
    // 依赖收集通过直接定义写死依赖关系,后续顺序引入
    for ( var i = 0; i < dependencies.length; i++ )
        this.load( dependencies[ i ], true );
    ​
    // scripts 依赖
    var scripts = {
        '_bootstrap': [
            'config', 'creators/inline', 'creators/themedui', 'editable', 'ckeditor', 'plugins',
            'scriptloader', 'style', 'tools', 'promise', 'selection/optimization', 'tools/color',
    ​
            // The following are entries that we want to force loading at the end to avoid dependence recursion.
            'dom/comment', 'dom/elementpath', 'dom/text', 'dom/rangelist', 'skin'
        ],
        'ckeditor': [
            'ckeditor_basic', 'log', 'dom', 'dtd', 'dom/document', 'dom/element', 'dom/iterator', 'editor', 'event',
            'htmldataprocessor', 'htmlparser', 'htmlparser/element', 'htmlparser/fragment', 'htmlparser/filter',
            'htmlparser/basicwriter', 'template', 'tools'
        ],
        ...
    }
    ​
    // 现在模块化开发比较成熟的情况下去看这样依赖收集的方式,难免看着觉得繁琐,并且后续在产出 bundle 时候还需要别的库(好像是CKBuilder 未深入查看)配合把这些依赖处理到一个 js 文件里面。后面有机会看的话再展开讲讲。
    
  4. core/loader.js load加载 _autoLoad: load载入 core/ckeditor.js

    // loader.js 底部直接加载了 core/ckeditor.js, 这是比较核心的文件了~
    // Check if any script has been defined for autoload.
    if ( CKEDITOR._autoLoad ) {
        CKEDITOR.loader.load( CKEDITOR._autoLoad );
        delete CKEDITOR._autoLoad;
    }
    
  5. 根据 loader 定义的依赖顺序,载入相应 core/xxx.js (dom, event, style, tools, plugins, env...)

  6. CKEDITOR.replace 在 core/creators/themedui.js 载入, replace调用 createInstance

    // 通过断点可以查询到, 创建实例的 api CKEDITOR.replace 在core/creators/themedui.js被定义,实际调用的是 createInstance函数
    CKEDITOR.replace = function( element, config ) {
        return createInstance( element, config, null, CKEDITOR.ELEMENT_MODE_REPLACE );
    };
    
  7. createInstance 内可以看到入口函数var editor = new CKEDITOR.editor( config, element, mode );

  8. CKEDITOR.editor 就是实例的构造函数,该函数定义于 core/editor.js function Editor

    CKEDITOR.editor = Editor;
    ​
    function Editor( instanceConfig, element, mode ) {
        ...
    }
    
  9. 构造函数通过ckeditor的 事件机制 加载

    • initConfig
    • onConfigLoaded
    • initComponents
    • loadSkin
    • loadLang
    • preloadStylesSet
    • loadPlugins
  10. core/editor.js 初始化完了之后触发 editor.fireOnce( 'loaded' ); 调用之前很多地方都注册过了的 editor.on( 'loaded', callback), 完成其他页面绘制等内容的加载。直到完成组件绘制

3. from chatGPT

下面是询问我的好兄弟chatGPT,它给出的 loader 加载流程。

  1. 加载 _bootstrap.js 文件:这个文件是 CKEditor 的入口文件,它定义了一些全局变量和函数,以及一些常用工具函数的封装,最终会执行 CKEDITOR.replace() 方法来替换页面中的 <textarea> 标签或者其他容器为编辑器实例。
  2. 加载 env.js 文件:这个文件主要是用于检测浏览器和设备的信息,并对一些环境变量进行设置,比如检测是否是 IE 浏览器,设置全局变量 CKEDITOR.env.
  3. 加载 config.js 文件:这个文件主要是用于配置 CKEditor 的一些参数和插件,它会根据用户传入的配置参数和默认配置参数进行合并,并将最终的配置对象保存在全局变量 CKEDITOR.config 中。
  4. 加载 lang.js 文件:这个文件用于加载 CKEditor 的语言文件,它会根据用户传入的语言代码和浏览器的默认语言进行匹配,并加载对应的语言文件,将语言数据保存在全局变量 CKEDITOR.lang 中。
  5. 加载 plugins.js 文件:这个文件用于加载 CKEditor 的插件,它会根据用户配置和默认配置来加载指定的插件,并将插件实例保存在全局变量 CKEDITOR.plugins 中。
  6. 加载 editor.js 文件:这个文件是 CKEditor 的核心文件,它定义了 CKEDITOR.editor 类,用于实例化编辑器对象。它会依次加载 dom.js, event.js, ui.js, selection.js, commands.js, styles.js 等文件,并将这些文件中定义的类和方法挂载到 CKEDITOR.editor 类的原型对象中,以供实例化编辑器对象时使用。
  7. 实例化编辑器对象:最终会执行 CKEDITOR.replace() 方法,该方法会实例化一个 CKEDITOR.editor 对象,并将其挂载到全局变量 CKEDITOR.instances 中,以供其他模块或插件使用。