道招

CKEditor系列(一)CKEditor4项目怎么跑起来的

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

CKEditor系列(一)CKEditor4项目怎么跑起来的

我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js

// src/core/ckeditor_base.js
if ( !window.CKEDITOR ) {

  window.CKEDITOR = ( function() {
    var basePathSrcPattern = /(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i;

    var CKEDITOR = {
      _: {
        pending: [],
        basePathSrcPattern: basePathSrcPattern
      },
      status: 'unloaded',
      basePath: ( function() {})(),
        // Find out the editor directory path, based on its <script> tag.
        var path = window.CKEDITOR_BASEPATH || '';

        return path;
      } )(),

      domReady: ( function() {
        // Based on the original jQuery code (available under the MIT license, see LICENSE.md).

        var callbacks = [];

        return function( fn ) {
          callbacks.push( fn );
        };

      } )()
    };

    return CKEDITOR;
  } )();
}

我对里面的代码进行了节选。

... // 上述src/core/ckeditor_base.js的压缩版
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>' );
    }

}

首次加载的时候其实是没有CKEDITOR.loader,所以进入上面的判断的else部分,会开始加载 core/loader.js,这是必要的准备工作里面的最后一环。

// core/loader.js
CKEDITOR.loader = ( function() {
    // Table of script names and their dependencies.
    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'
      ],
      'ckeditor_base': [],
      'ckeditor_basic': [ 'editor_basic', 'env', 'event' ],
      load: function( scriptName, defer ) {
        // Check if the script has already been loaded.
        if ( ( 's:' + scriptName ) in this.loadedScripts )
          return;

        // Get the script dependencies list.
        var dependencies = scripts[ scriptName ];
        if ( !dependencies )
          throw 'The script name"' + scriptName + '" is not defined.';

        // Mark the script as loaded, even before really loading it, to
        // avoid cross references recursion.
        // Prepend script name with 's:' to avoid conflict with Array's methods.
        this.loadedScripts[ 's:' + scriptName ] = true;

        // Load all dependencies first.
        for ( var i = 0; i < dependencies.length; i++ )
          this.load( dependencies[ i ], true );

        var scriptSrc = getUrl( 'core/' + scriptName + '.js' );

        // Append the <script> element to the DOM.
        // If the page is fully loaded, we can't use document.write
        // but if the script is run while the body is loading then it's safe to use it
        // Unfortunately, Firefox <3.6 doesn't support document.readyState, so it won't get this improvement
        if ( document.body && ( !document.readyState || document.readyState == 'complete' ) ) {
          pendingLoad.push( scriptName );

          if ( !defer )
            this.loadPending();
        } else {
          // Append this script to the list of loaded scripts.
          this.loadedScripts.push( scriptName );

          document.write( '<script src="' + scriptSrc + '" type="text/javascript"><\/script>' );
        }
      }
    };
  } )();

我们可以看到,这里面定义需要加载的js脚本的名字和它对应的依赖。

现在就通过CKEDITOR.loader.load( 'ckeditor' )来加载ckeditor(当然,根据上面的代码可以看出,core/ckeditor也有依赖,依赖加载完了才能真的轮到它),这一切就正式开始了。

core/ckeditor里面还会加载core/_bootstrap,我们可以看到CKEditor的代码基本都是按照自执行函数的写法写的。

接下来我们看看具体在目前主流框架里面的启动部分,以React为例

react4-react的源码里面我们可以看到。

// src/useCKEditor.ts
const initEditor = ( CKEDITOR: CKEditorNamespace ) => {
    const isInline = typeRef.current === 'inline';
    const isReadOnly = configRef.current.readOnly;

    /**
    * Dispatches `beforeLoad` event.
    */
    if ( subscribeToRef.current.indexOf( 'beforeLoad' ) !== -1 ) {
    dispatchEventRef.current?.( {
    type: CKEditorEventAction.beforeLoad,
    payload: CKEDITOR
    } );
    }

    const editor = CKEDITOR[ isInline ? 'inline' : 'replace' ](
    element,
    configRef.current
    );
}

我们一般使用的默认的编辑器模式即classic,所以上面我们调用CKEDITOR['replace'](element,configRef.current)来开始创建编辑器实例

// src/core/creator/themeui.js
    CKEDITOR.replace = function( element, config ) {
        return createInstance( element, config, null, CKEDITOR.ELEMENT_MODE_REPLACE );
    };

    function createInstance( element, config, data, mode ) {
        element = CKEDITOR.editor._getEditorElement( element );

        if ( !element ) {
            return null;
        }

        // (#4461)
        if ( CKEDITOR.editor.shouldDelayEditorCreation( element, config ) ) {
            CKEDITOR.editor.initializeDelayedEditorCreation( element, config, 'replace' );
            return null;
        }

        // Create the editor instance.
        var editor = new CKEDITOR.editor( config, element, mode );

        if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
            // Do not replace the textarea right now, just hide it. The effective
            // replacement will be done later in the editor creation lifecycle.
            element.setStyle( 'visibility', 'hidden' );

            // https://dev.ckeditor.com/ticket/8031 Remember if textarea was required and remove the attribute.
            editor._.required = element.hasAttribute( 'required' );
            element.removeAttribute( 'required' );
        }

        data && editor.setData( data, null, true );

        // Once the editor is loaded, start the UI.
        editor.on( 'loaded', function() {
            if ( editor.isDestroyed() || editor.isDetached() ) {
                return;
            }

            loadTheme( editor );

            if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE && editor.config.autoUpdateElement && element.$.form )
                editor._attachToForm();

            editor.setMode( editor.config.startupMode, function() {
                // Clean on startup.
                editor.resetDirty();

                // Editor is completely loaded for interaction.
                editor.status = 'ready';
                editor.fireOnce( 'instanceReady' );
                CKEDITOR.fire( 'instanceReady', null, editor );
            } );
        } );

        editor.on( 'destroy', destroy );
        return editor;
    }

终于可以看看Editor构造函数了

// core/editor.js
function Editor( instanceConfig, element, mode ) {
    // Call the CKEDITOR.event constructor to initialize this instance.
    CKEDITOR.event.call( this );

    // Make a clone of the config object, to avoid having it touched by our code. (https://dev.ckeditor.com/ticket/9636)
    instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig );
    // Declare the private namespace.
    this._ = {};

    this.commands = {};

    this.templates = {};

    this.name = this.name || genEditorName();

    /**
     * A unique random string assigned to each editor instance on the page.
     *
     * @readonly
     * @property {String}
     */
    this.id = CKEDITOR.tools.getNextId();

    this.status = 'unloaded';

    this.config = CKEDITOR.tools.prototypedCopy( CKEDITOR.config );

    /**
     * The namespace containing UI features related to this editor instance.
     *
     * @readonly
     * @property {CKEDITOR.ui}
     */
    this.ui = new CKEDITOR.ui( this );

    this.focusManager = new CKEDITOR.focusManager( this );

    /**
     * Controls keystroke typing in this editor instance.
     *
     * @readonly
     * @property {CKEDITOR.keystrokeHandler}
     */
    this.keystrokeHandler = new CKEDITOR.keystrokeHandler( this );

    // Make the editor update its command states on mode change.
    this.on( 'readOnly', updateCommands );
    this.on( 'selectionChange', function( evt ) {
      updateCommandsContext( this, evt.data.path );
    } );
    this.on( 'activeFilterChange', function() {
      updateCommandsContext( this, this.elementPath(), true );
    } );
    this.on( 'mode', updateCommands );

    // Optimize selection starting/ending on element boundaries (#3175).
    CKEDITOR.dom.selection.setupEditorOptimization( this );

    // Handle startup focus.
    this.on( 'instanceReady', function() {
      if ( this.config.startupFocus ) {
        if ( this.config.startupFocus === 'end' ) {
          var range = this.createRange();
          range.selectNodeContents( this.editable() );
          range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
          range.collapse();
          this.getSelection().selectRanges( [ range ] );
        }
        this.focus();
      }
    } );

    CKEDITOR.fire( 'instanceCreated', null, this );

    // Add this new editor to the CKEDITOR.instances collections.
    CKEDITOR.add( this );

    // Return the editor instance immediately to enable early stage event registrations.
    CKEDITOR.tools.setTimeout( function() {
      if ( !this.isDestroyed() && !this.isDetached() ) {
        initConfig( this, instanceConfig );
      }
    }, 0, this );
  }

我们可以看到最后面有initConfig,根据配置开始生成出我们的编辑器了。 接下来的路径大致就如下了,里面还会穿插一些对当且阶段完成了的通知消息:

  • initConfig
  • onConfigLoaded
  • initComponents
  • loadSkin
  • loadLang
  • preloadStylesSet
  • loadPlugins

终于到我们的加载插件部分了。。。 插件加载完了,就可以通知宣布这个实例已经加载好了

// core/editor.js
editor.status = 'loaded';
editor.fireOnce( 'loaded' );

此前不少插件就已经监听loaded事件了,比如

// core/creator/themeui.js
        editor.on( 'loaded', function() {
            if ( editor.isDestroyed() || editor.isDetached() ) {
                return;
            }

            loadTheme( editor );
        }

loadTheme方法中会触发uiSpace消息,而我们工具栏(core/toolbar.js)就是在监听到这个消息后,才开始对工具栏各操作按钮的生成的。

// plugins/toolbar/plugin.js
editor.on( 'uiSpace', function( event ) {
    for ( var r = 0; r < toolbarLength; r++ ) {
        // Create all items defined for this toolbar.
        for ( var i = 0; i < items.length; i++ ) {
            function addItem( item ) { // jshint ignore:line
                var itemObj = item.render( editor, output );
                index = toolbarObj.items.push( itemObj ) - 1;
            }
            addItem( item );
        }
    }

}

记住上面的item.render,我们的工具栏就是调用各插件的render来生成的工具栏。

好,后续部分下次再写了。

更新时间:
上一篇:下一篇:

相关文章

关注道招网公众帐号
道招开发者二群