1

CKEditor4是怎么跑起来的(一)

 3 years ago
source link: https://www.daozhao.com/10174.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

CKEditor4是怎么跑起来的(一)

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

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来生成的工具栏。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK