-
Notifications
You must be signed in to change notification settings - Fork 12
Modularity
⚠⚠ ⚠⚠ ⚠⚠
This wiki served in the early days of CKEditor 5 development and can be severely outdated.
Refer to the official CKEditor 5 documentation for up-to-date information.
The CKEditor 5 architecture is based on an important aspect – modularity.
Previous versions of CKEditor were based on a custom plugin system. It allowed to create an extensible infrastructure, it wasn't however sufficient for creating reusable components and distributing them in a convenient manner.
- Plugins were "distributable entities".
- Plugins could contain only one JavaScript file (actually, they could contain more, but there was no easy way to load them), what forced creating heavy plugin files and made reusing components harder.
- Lack of proper modularity didn't allow to produce highly optimized builds.
CKEditor 5 is meant to fix this situation.
We started developing CKEditor 5 using AMD. It was our first choice because at that moment the only alternative was the CommonJS modules which because of synchronous require()
method need a build step in order to be used in the browser. Other than that, CommonJS modules have no advantages over AMD.
Unfortunately, it turned out that the architecture based on AMD is overcomplicated due to the complicated structure of the project. Resolving paths, circular dependencies, building for other environments (such as Node.js), processing and validating the code in general were tough. At the same time we felt that initial implementation of the plugin architecture didn't work perfectly as well.
Fortunately, in the meantime ES6 modules were defined in ES2015 specification. Finally, JavaScript has been given a native syntax for defining modules. At the moment of writing this page ES6 modules aren't yet usable natively as no browser supports them. That's due to the fact that ES2015 defines only the syntax and its semantics, without defining how those modules can be loaded. The new syntax however has many advantages: it can be statically analyzed, it can be transpiled (although not all of its features) to other formats such as AMD and CommonJS, it solves circular dependencies problem and it's much cleaner than AMD.
In January 2016 we finished porting code to ES6 modules. That required introducing a building step to the workflow, it did however significantly cleaned the architecture and opened many new possibilities such as building to many formats and out-of-the-box compatibility with many other JavaScript solutions which also adopt ES6 modules.
A definition of a feature module located in ckeditor5-image/src/captionedimage.js
could look like this:
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
'use strict';
import Feature from '../core/feature.js'; // Imports ckeditor5-core/src/feature.js
import BaseImage from './baseimage.js'; // Imports ckeditor5-image/src/baseimage.js
/**
* ... API docs here...
*/
export default class CaptionedImage extends Feature {
... class definition goes here ...
}
As you can see in the example above the paths used in import
statements look differently than the real paths. However, such module (once ES6 modules are supported by browsers) would work without any tricks. How is this possible? The source files are scattered over the file system due to the packaging system and various building scenarios. The builder copies all files to a directory structure which is much simpler and is fully predictable.
The file structure produced by the building system can be loaded straight into Node.js, however, you will want to bundle the code before using it in a browser on the production server.
By concatenating multiple JS files we lose the information about paths, hence any way to identify separate modules. While in some highly optimized systems this knowledge is not needed (see e.g. this example), there are many use cases in which a it must be possible to import specific modules from a CKEditor distribution.
This means that for bundling we need to introduce named modules. The directory structure created during the building process was defined to best reflect the namespaces in such a bundle. For instance, we can imagine that a bundle for AMD will include such modules:
-
ckeditor
->dist/amd/ckeditor.js
-
ckeditor5/core/editor
->dist/amd/ckeditor5/core/editor.js
- etc...
To use such a bundle you will only need to include it using a <script>
tag and then import the modules:
require( [ 'ckeditor', 'ckeditor5/core/editor' ], function( CKEDITOR, Editor ) {
CKEDITOR.create( ... );
} );
In all scenarios which integrate CKEditor building process and allow to quickly toggle between a production environment and development environment debugging will not be a problem.
However, in many scenarios a CKEditor bundle will be used directly. Three things can be done (depending on how bad the things look ;)):
- Using source maps.
- A switch to a non-minified bundle.
- A switch to a source version of CKEditor.
The first 2 methods should satisfy many scenarios when a CKEditor is integrated with a web page. However, when at the same time a developer wants to work on an additional CKEditor package, then:
- Either this package is implemented already in an AMD or similar format, so matching the environment of the web page. In this case the module being developed will be using a named dependencies. However, such module will be usable only in this certain environment.
- Or the web page should be able to switch between a production and source version of CKEditor (and the package being developed). This case unfortunately is very tricky due to the need to create a mapping between named modules and their paths. We weren't able to achieve such results with Require.JS (most popular AMD loader) without overriding a non-public method. However, it seems to be doable.
Generally speaking, the less unused code in the bundle the better. This should happen on multiple layers:
- The developer should not include packages and features which are not necessary.
- The bundler should not include modules which are not used by the configuration of features provided by the developer.
We hope (and aim for) that the current approach to building will allow to use any code bundler (such as Rollup or Webpack). This means that the bundling step must not require any special logic and the bundler should be able to resolve automatically which modules are necessary for the given "entry-points" (such as ckeditor.js
and chosen features, theme, etc.).
The above means that we need to avoid creating any indirect, magical dependencies or indexes of modules (modules importing other modules but not using them). If some module is necessary, the modules which require it should clearly import it and they should not import any other modules. This rule will definitely affect some design choices, pushing more responsibilities to the builder.