Closed Mati365 closed 2 months ago
@pszczesniak
Another thought - what about with plugins that are not published on NPM? I know that it's some kind of rare edge case but we got one - ckeditor5-mermaid, i know it's marked as experimental but do we want to only shrug to our potential clients?
const cloud = useCKEditorCloud({
version: '42.0.1',
plugins: {
YourPlugin: {
getExportedEntries: () => import( './your-plugin' ),
},
},
});
It can be done using async imports + proper globals
configuration in bundlers (we have to point that ckeditor5
is CKEDITOR
window dependency).
Thx @gorzelinski! I applied fixes.
Totals | |
---|---|
Change from base Build b63e3319-c29c-469a-9a26-761f5c32893e: | 0.0% |
Covered Lines: | 582 |
Relevant Lines: | 582 |
React CDN Integration
Related Vue integration: https://github.com/ckeditor/ckeditor5-vue/pull/301 Related Angular integration: https://github.com/ckeditor/ckeditor5-angular/pull/431
❓ Tl;dr
The
useCKEditorCloud
hook is designed for integratingCKEditor
from a CDN into React applications, aiming to simplify the process of addingCKEditor
by managing its asynchronous loading, handling errors, and providing access to the editor and its dependencies once loaded. This addresses the common challenge in React and other frameworks of integrating third-party JavaScript libraries, which involves managing asynchronous script loading and ensuring scripts and styles are loaded in the correct order without blocking the main thread.Additionally, the hook specifically tackles the challenges associated with loading
CKEditor
from its CDN. It optimizes the loading process by preventing redundant script injections and handling race conditions and caching effectively. This ensures that scripts are not injected multiple times if they are already present, which is crucial for applications that dynamically destroy and reinitialize components, includingCKEditor
, as users navigate. This approach enhances performance and user experience by ensuring efficient and error-free loading ofCKEditor's
assets from the CDN.Demos
Plain editor from CDN (with external plugins): http://localhost:5174/demos/cdn-react/index.html Multiroot hook: http://localhost:5174/demos/cdn-multiroot-react/index.html
🔧 General format of the
useCKEditorCloud
hook callThe
useCKEditorCloud
hook is responsible for returning information that:CKEditor
is still being downloaded from the CDN withstatus = "loading"
.status = "error"
, then further information is returned in theerror
field.data
field and its dependencies whenstatus = "success"
.Simplest config
The simplest usage example that will load
CKEditor
fromCDN
and return its data incloud.data
:CKBox config
CKBox integration example:
Third party plugins config
A more advanced example that allows specify whether external stylesheets or scripts should be loaded:
⚠️ Potential blockers
Window
typings ofCKEditor 5
are not present. So we have to manually re-export them:which is problematic when we want use type with
prototype
(likeEventInfo
):So at this moment we have this:
and later:
🔧 Operation algorithm of
useCKEditorCloud
Dependency packages
Under the hood, the hook uses objects describing the dependencies of bundles served in the CDN. These objects define which styles and which scripts must be injected on the page to receive objects on the window returned by
getExportedEntries
. In other words, to download a bundle fromCKEditor5
, we pass the link to our CDN inscripts
, similarly instylesheets
, and ingetExportedEntries
we return thewindow.CKEDITOR5
object. This object will then be returned by the hook after loading is complete.Package format:
Currently, we have two pre-defined packages:
Defined by
createCKCdnBaseBundlePack
, the main editor package, which returns:Defined by
createCKCdnPremiumBundlePack
, the package with premium editor features, which returns:Depending on the provided configuration, the
useCKEditorCloud
hook merges these packages using thecombineCKCdnBundlesPacks
method and transforms them into this form:This form is extended by other UMD scripts (like
WIRIS
) and thanks to exports fromcreateCKCdnBaseBundlePack
, it allows specifying whether users want to download only styles, only JS, or both. By merging, we get the ability to load allcss
styles andjs
scripts before starting to load the main code.HTML Injection
Preload
At the moment of the first render of the
App
component, theuseCKEditorCloud
will inject tags responsible forrel=preload
into thehead
:This informs the browser that while downloading
.css
files, it can also start downloading.umd.js
files. This counteracts a dependency cascade where we would have to wait for the mainCKEditor
bundle to load, then theCKEditor Premium Features
bundle, and then some custom plugin code before starting the editor initialization.preload
loads them all in parallel. This is noticeable on slower connections, it is not required for the implementation itself, but significantly improves UX by about 500-600ms on load atFast 3G
speed.Target resources
When the hook has preloaded the resources, it proceeds to load the target scripts and creates a
Promise
from this. This is problematic because React does not support asynchronicity in components in thestable
version. In theunstable
version, it has a hookuse
, which currently cannot be used because we supportReact 16
, which does not support it.At this stage, it looks like this:
Dependency packages are merged. All CSS styles are loaded, then JS, and finally plugins.
Handling Asynchrony in React
Since the handling of
Promise
inReact
withoutsuspense
does not exist, it is managed through a customly added hookuseAsyncValue
. Its usage looks roughly like thisAfter finishing loading, it will return:
However, the hook also has
loading
,idle
, anderror
states. The hook, with each change ofserializedConfigKey
, which is the config's form serialized to a string, reloads the data from the CDN. This allows controlling the loading of premium plugins and recreating the editor.Handling race-conditions and cache
Each injecting function has a verification whether the script has already been embedded or not. Example for JS tags:
thanks to this, destroying the component and initializing it at the moment when it was in the process of injecting the script does not cause problems and works. Navigation between pages that also have the same dependencies has been accelerated. This gives us the possibility to have 2 editors on the site - one having premium features, the other not.
🔧 General format of the
withCKEditorCloud
HOCThe main reason for the creation of this
HOC
is thehook
useMyMultiRootEditor
. Thanks to it, we avoid a situation where we have to add conditional rendering in theuseMyMultiRootEditor
hook:In theory, modifying
useMultiRootEditor
is possible, but it would potentially cause problems with race-conditions and the stability of the integration itself at the time of quick loading flag switchescloud
. UsingHOC
, we avoid conditioning, and the embedding component already has thecloud
injected with initializedCKEditor
constructors: