Ckeditor4源码分析之入口文件和加载器

282 阅读3分钟

一、入口文件ckeditor.js

入口文件主要由两部分代码,第一部分是对CKEDITOR对象的定义,并挂载到window上。

if (!window.CKEDITOR) {
  window.CKEDITOR = (function () {
    var CKEDITOR = {
      timestamp: '',
      version: '',
      status: 'unloaded',
      getUrl: function () {
       ...
      }
     ...
    }
    return CKEDITOR;
  })()
}

第二部分代码加载core/loader.js, 并通过loader加载ckeditor。

这里有个判断如果有CKEDITOR.loader就直接加载ckeditor.js,否则就通过CKEDITOR._autoLoad = 'ckeditor'; 等loader加载完之后自动加载ckeditor。

if ( CKEDITOR.loader )
	CKEDITOR.loader.load( 'ckeditor' );
else{
  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.skinName = 'moono-lisa';

二、加载器 core/loader.js

if ( !CKEDITOR.loader ) {
    CKEDITOR.loader = (function(){...})();
}

// 自动加载ckeditor
if ( CKEDITOR._autoLoad ) {
	CKEDITOR.loader.load( CKEDITOR._autoLoad );
	delete CKEDITOR._autoLoad;
}

这里就是加载器的代码了,加载器的主要功能是加载模块的所有依赖模块,然后加载这个模块。所以其内部存放着模块的依赖关系:

  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' ],
    'command': [],
    'config': [ 'ckeditor_base' ],
    'dom': [],
    'dom/comment': [ 'dom/node' ],
    'dom/document': [ 'dom/node', 'dom/window' ],
    'dom/documentfragment': [ 'dom/element' ],
    'dom/element': [ 'dom', 'dom/document', 'dom/domobject', 'dom/node', 'dom/nodelist', 'tools' ],
    'dom/elementpath': [ 'dom/element' ],
    'dom/event': [],
    'dom/iterator': [ 'dom/range' ],
    'dom/node': [ 'dom/domobject', 'tools' ],
    'dom/nodelist': [ 'dom/node' ],
    'dom/domobject': [ 'dom/event' ],
    'dom/range': [ 'dom/document', 'dom/documentfragment', 'dom/element', 'dom/walker' ],
    'dom/rangelist': [ 'dom/range' ],
    'dom/text': [ 'dom/node', 'dom/domobject' ],
    'dom/walker': [ 'dom/node' ],
    'dom/window': [ 'dom/domobject' ],
    'dtd': [ 'tools' ],
    'editable': [ 'editor', 'tools' ],
    'editor': [
      'command', 'config', 'editor_basic', 'filter', 'focusmanager', 'keystrokehandler', 'lang',
      'plugins', 'tools', 'ui'
    ],
    'editor_basic': [ 'event' ],
    'env': [],
    'event': [],
    'filter': [ 'dtd', 'tools' ],
    'focusmanager': [],
    'htmldataprocessor': [ 'htmlparser', 'htmlparser/basicwriter', 'htmlparser/fragment', 'htmlparser/filter' ],
    'htmlparser': [],
    'htmlparser/comment': [ 'htmlparser', 'htmlparser/node' ],
    'htmlparser/element': [ 'htmlparser', 'htmlparser/fragment', 'htmlparser/node' ],
    'htmlparser/fragment': [ 'htmlparser', 'htmlparser/comment', 'htmlparser/text', 'htmlparser/cdata' ],
    'htmlparser/text': [ 'htmlparser', 'htmlparser/node' ],
    'htmlparser/cdata': [ 'htmlparser', 'htmlparser/node' ],
    'htmlparser/filter': [ 'htmlparser' ],
    'htmlparser/basicwriter': [ 'htmlparser' ],
    'htmlparser/node': [ 'htmlparser' ],
    'keystrokehandler': [ 'event' ],
    'lang': [],
    'log': [ 'ckeditor_basic' ],
    'plugins': [ 'resourcemanager' ],
    'resourcemanager': [ 'scriptloader', 'tools' ],
    'scriptloader': [ 'dom/element', 'env' ],
    'selection': [ 'dom/range', 'dom/walker' ],
    'skin': [],
    'style': [ 'selection' ],
    'template': [],
    'tools': [ 'env' ],
    'ui': [],
    'creators/themedui': [],
    'creators/inline': [],
    'promise': [ 'tools' ],
    'selection/optimization': [ 'selection' ],
    'tools/color': [ 'tools' ]
  };

加载器本质上就是一个对象:

{
  loadedScripts: [], // 已加载的模块
  scripts: scripts, // 模块的依赖关系
  loadPending: function(){...}, //
  load: function(scriptName, defer){...}, // 加载模块的主函数
}

加载功能具体实现是在load函数中:

  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>' );
      }
    }

loadPending函数用来加载被延迟加载的模块:

    loadPending: function() {
      var scriptName = pendingLoad.shift();

      if ( !scriptName )
        return;

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

      var script = document.createElement( 'script' );
      script.type = 'text/javascript';
      script.src = scriptSrc;

      function onScriptLoaded() {
        // Append this script to the list of loaded scripts.
        CKEDITOR.loader.loadedScripts.push( scriptName );

        // Load the next.
        CKEDITOR.loader.loadPending();
      }

      // We must guarantee the execution order of the scripts, so we
      // need to load them one by one. (https://dev.ckeditor.com/ticket/4145)
      // The following if/else block has been taken from the scriptloader core code.
      if ( typeof script.onreadystatechange !== 'undefined' ) {
        /** @ignore */
        script.onreadystatechange = function() {
          if ( script.readyState == 'loaded' || script.readyState == 'complete' ) {
            script.onreadystatechange = null;
            onScriptLoaded();
          }
        };
      } else {
        /** @ignore */
        script.onload = function() {
          // Some browsers, such as Safari, may call the onLoad function
          // immediately. Which will break the loading sequence. (https://dev.ckeditor.com/ticket/3661)
          var timer = setTimeout( function() {
            // Once the script is loaded, remove the listener as this might lead to memory leaks (#589).
            script.onload = null;
            onScriptLoaded( scriptName );
            clearTimeout(timer);
          }, 0 );
        };
      }

      document.body.appendChild( script );
    },

load的具体过程:

  • 检查模块是否已经加载过,如果加载过就直接返回
  • 检查模块是在依赖表中存在,如果不存在就抛出异常
  • 获取模块的依赖关系,开始深度优先遍历加载模块的所有依赖模块
  • 加载模块本身 最终通过document.write或document.body.appendChild( script )加载模块