
CKEditor 5 Decoupled Editor 完全指南自由定制文档编辑器 UI 布局与集成实战【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5导读Decoupled分离式编辑器是 CKEditor 5 中一种只提供能力、不限定位置的编辑器实现它同时提供内联可编辑区域editable与工具栏toolbar但默认不把任何 UI 组件渲染到 DOM 中完全由开发者决定把工具栏、菜单栏、编辑区分别放置到页面的哪个位置。本指南以 packages/ckeditor5-editor-decoupled/docs/api/editor-decoupled.md 为骨架结合仓库内 DecoupledEditor 源码、文档编辑器实战教程 与测试用例带你掌握DecoupledEditor 的三种初始化方式、root配置的完整参数语义、工具栏与菜单栏的挂载方法、销毁时 DOM 的清理时机以及如何基于它搭建一个类似原生文字处理软件的文档编辑器。DecoupledEditor 是什么设计与适用场景从源码注释可以看到官方对该编辑器的定位见 decouplededitor.ts分离式编辑器实现。它提供一个内联可编辑区域和一个工具栏。但与其它编辑器不同除非显式配置它不会在 DOM 中渲染这些组件。这意味着它天生面向需要自定义 UI、结构开放、允许开发者精确指定界面位置的集成场景。典型应用包括文档编辑器Document Editor把工具栏悬浮在纸张上方编辑区做成一张居中的 A4 纸见 文档编辑器示例内容由客户端动态生成的页面DOM 结构在初始化时刻尚未就绪可以先用纯数据创建一个脱离文档的编辑器实例等容器就绪后再把 UI 元素手动插入需要把工具栏与编辑区放在页面不同区块的复杂布局例如工具栏固定在页面顶部、编辑区嵌在表单中间。与 ClassicEditor 的最大区别在于ClassicEditor 会把工具栏和编辑区自动包裹在同一个根容器内并自动插入 DOM而 DecoupledEditor 把插到哪、怎么排的决定权完全交给集成方。本包在 包索引 中只导出三个符号DecoupledEditor、DecoupledEditorUI、DecoupledEditorUIView。安装与引入本包是 CKEditor 5 开源聚合包的一部分直接安装ckeditor5即可使用npm install ckeditor5随后在代码中引入import { DecoupledEditor } from ckeditor5;仓库内独立的源码包位于packages/ckeditor5-editor-decouplednpm 名称为ckeditor/ckeditor5-editor-decoupled其依赖为ckeditor/ckeditor5-core、ckeditor/ckeditor5-engine、ckeditor/ckeditor5-ui、ckeditor/ckeditor5-utils见 package.json。在 monorepo 中开发调试时也可以直接相对路径引入源码import { DecoupledEditor } from ../src/decouplededitor.js;这与仓库手动测试脚本 decouplededitor.ts 的用法一致。架构剖析三个类的职责与初始化调用链从源码结构看本包的核心由三个类组成它们构成了 DecoupledEditor 的完整运行骨架。DecoupledEditor编辑器主类DecoupledEditor 继承自ElementApiMixin( Editor )由ckeditor/ckeditor5-core提供因此天然具备setData()/getData()数据接口与updateSourceElementOnDestroy等元素同步能力。其静态属性editorName返回DecoupledEditor测试 decouplededitor.js 专门验证了这一标识。构造过程的关键步骤见 decouplededitor.ts通过normalizeSingleRootEditorConstructorParams归一化构造参数兼容老式元素/字符串 config两参签名通过normalizeRootsConfig把root/roots/initialData/label/placeholder等新旧配置形态统一为roots.main.*规范形式若roots.main.element是真实 DOM 元素则记为该编辑器的sourceElement并调用secureSourceElement防止同一元素被两个编辑器实例占用在模型文档上创建主根this.model.document.createRoot根名称默认为main依据toolbar.shouldNotGroupWhenFull决定工具栏是否启用溢出自动分组默认启用实例化DecoupledEditorUIView与DecoupledEditorUI。create()静态方法是唯一推荐的初始化入口源码明确提示不要直接调用构造函数见 decouplededitor.ts。其异步流程为见 decouplededitor.tsnew DecoupledEditor(...) → await editor.initPlugins() // 加载插件此时 schema 才完整 → verifyRootElements( editor ) // 校验根元素是否为合法的 limit 元素 → await editor.ui.init() // 渲染 UI 视图、绑定编辑根 → await editor.data.init( initialData ) // 初始化数据 → editor.fire( ready ) // 触发 ready 事件其中verifyRootElements特意放在插件初始化之后执行是因为自定义根元素名如由插件注册的 modelElement在构造阶段可能尚未注册进 schemadecouplededitor.ts 有明确注释说明。DecoupledEditorUIView纯虚拟的 UI 视图DecoupledEditorUIView 继承自EditorUIView是一个虚拟视图——它持有三个子组件但不对它们在 DOM 中的排列做任何假设toolbar主工具栏ToolbarView默认启用shouldGroupWhenFullmenuBarView菜单栏MenuBarVieweditable内联可编辑区域InlineEditableUIView。值得注意的实现细节见 decouplededitoruiview.ts由于工具栏可能被放置在页面的任意位置其祖先元素未必有正确的字体或dir属性因此源码为工具栏与菜单栏统一追加了ck-reset_all、ck-rounded-corners类并强制设置dir: locale.uiLanguageDirection以保证 UI 在任意宿主环境下样式一致、圆角正常、RTL 语言方向正确。DecoupledEditorUIUI 与引擎的接线层DecoupledEditorUI 继承自EditorUI在init()中完成关键的接线工作见 decouplededitorui.ts为编辑区设置name与编辑根同名用于 ARIA 等识别通过rootAcceptsBlocks判断当前根是否接受块级内容进而决定editable.isInlineRoot调用view.render()渲染视图——只有当DecoupledEditor.create()传入了真实 DOM 元素时编辑区 DOM 才会在此前已存在通过this.setEditableElement()注册编辑区把可编辑区的isFocused绑定到全局focusTracker保证工具栏、下拉框等获得焦点时编辑区仍保持聚焦样式关键步骤editingView.attachDomRoot( editableElement )即把引擎的编辑视图根挂到 DOM 编辑区上这是引擎与 UI 相遇的入口初始化占位符_initPlaceholder与工具栏_initToolbartoolbar.fillFromConfig从config.toolbar填充按钮并通过addToolbar注册以便支持AltF10、Esc键盘导航初始化菜单栏menuBarView后触发EditorUI#ready事件。正因为 UI 的所有组件都是独立存在的没有统一包裹容器ui.init()之后你就能分别拿到editor.ui.view.toolbar.element、editor.ui.view.menuBarView.element与editor.ui.getEditableElement()并把它们挂到任意位置。DecoupledEditor.create() 的三种初始化方式DecoupledEditor.create()支持两种新式签名与一种兼容性签名。以下均以新式单配置对象签名为准旧签名已被标记deprecated将在未来版本移除。方式一基于已有 DOM 元素内容自动作为初始数据传入root.element指向一个已存在于页面中的容器其内容会作为编辑器初始数据元素本身会成为可编辑区域DecoupledEditor .create( { root: { element: document.querySelector( #editor ) } } ) .then( editor { console.log( Editor was initialized, editor ); // 把工具栏挂到 body 上。 document.body.appendChild( editor.ui.view.toolbar.element ); } ) .catch( err { console.error( err.stack ); } );方式二纯数据创建detached脱离文档不提供 DOM 元素而是直接传root.initialData。此时编辑器完全脱离文档创建工具栏与可编辑区域都需要你手动插入页面DecoupledEditor .create( { root: { initialData: pHello world!/p } } ) .then( editor { console.log( Editor was initialized, editor ); // 工具栏手动挂载。 document.body.appendChild( editor.ui.view.toolbar.element ); // 因为提供了初始数据而非元素编辑区也要手动插入。 document.body.appendChild( editor.ui.getEditableElement() ); } ) .catch( err { console.error( err.stack ); } );这种方式适合页面内容由客户端动态生成、初始化时 DOM 尚未就绪的场景——可以先创建编辑器实例等容器可用后再把 UI 元素追加进去。方式三已有元素 配置中的初始数据混合如果你希望使用某个已存在的元素作为编辑区但内容由配置提供例如集成代码不方便写死元素内部 HTML可以混合使用DecoupledEditor .create( { root: { element: document.querySelector( #editor ), initialData: h2Initial data/h2pFoo bar./p } } ) .then( editor { document.body.appendChild( editor.ui.view.toolbar.element ); } ) .catch( err { console.error( err.stack ); } );注意初始数据只能有一个来源。测试 decouplededitor.js 验证了以下冲突会抛出对应CKEditorError场景错误代码构造参数传了数据字符串同时 config 里又设了initialDataeditor-create-initial-data-overspecified构造参数传了数据字符串同时root.initialDataeditor-create-root-initial-data-overspecified构造参数传了数据字符串同时roots.main.initialDataeditor-create-root-initial-data-overspecified同时设置root与roots.maineditor-create-roots-with-main同时设置旧initialData与root.initialDataeditor-create-legacy-initial-data-overspecified同时传入 source 元素与root.elementeditor-create-root-element-overspecified此外如果根元素是textarea或input创建会抛出editor-wrong-element对同一 source 元素重复初始化会抛出editor-source-element-already-used见 decouplededitor.js。root 配置项全解element、initialData、placeholder、label 与更多root或规范形式roots.main是本编辑器的核心配置源码测试完整覆盖了其语义element编辑区元素的三种形态root.element可省略或为以下三种形态之一省略默认创建一个div作为编辑区见 decouplededitor.jsDOM 元素document.querySelector(#editor)元素内容作为初始数据且元素成为编辑区本体sourceElement会被记录标签名字符串如root: { element: h1 }此时编辑器会按该标签创建编辑区sourceElement保持undefined字符串本身不会被当作初始数据见 decouplededitor.js视图元素定义对象如root: { element: { name: section, classes: [ foo ], styles: {...}, attributes: { data-id: 123 } } }。classes数组或字符串会叠加在 CKEditor 自身的ck ck-content类之上styles对象优先于attributes.style字符串attributes.class与顶层classes会合并见 decouplededitor.js。initialData初始内容作为编辑器的初始数据。若未设置则取 DOM 元素内容方式一或构造参数传入的数据若已设置不会被 DOM 元素内容覆盖见 decouplededitor.js。placeholder占位提示文本root.placeholder兼容旧式顶层placeholder会归一化为roots.main.placeholder并在 UI 初始化时通过enableViewPlaceholder启用占位符见 decouplededitorui.ts。测试见 decouplededitor.js。label可访问性标签aria-labelroot.label兼容旧式顶层label字符串或{ main: ... }对象会作为编辑区视图的aria-label。默认值为Rich Text Editor. Editing area: main若源 DOM 元素自身已有aria-label未配置 label 时该值会被保留测试 decouplededitor.js 覆盖了全部取值分支。modelAttributes模型根属性root.modelAttributes可以给模型根设置自定义属性之后可通过editor.getRootAttributes()读取。手动测试 decouplededitor.ts 中即演示了modelAttributes: { section: intro }的用法对应测试见 decouplededitor.js。modelElement自定义根模型元素名root.modelElement允许指定主根的模型元素名默认是$root。但注意该元素必须在 schema 中注册为 limit 元素否则create()会以editor-root-element-is-not-limit错误拒绝见 decouplededitor.js。工具栏配置工具栏通过顶层toolbar配置支持toolbar.items按钮列表与toolbar.shouldNotGroupWhenFull关闭溢出自动分组。测试 decouplededitor.js 验证了默认shouldGroupWhenFull为true可通过shouldNotGroupWhenFull: true关闭。更完整的工具栏说明见 工具栏配置指南。实战用 DecoupledEditor 构建文档编辑器官方在 document-editor 教程 中演示了如何在 DecoupledEditor 之上搭建一个具有原生文字处理软件外观的文档编辑器成品示例见 文档编辑器示例。下面是完整可运行的构建流程。1. 初始化编辑器教程中的初始化方式与上面方式一完全一致把工具栏挂到页面预先准备的.document-editor__toolbar容器中import { DecoupledEditor } from ckeditor5; DecoupledEditor.create( { root: { element: document.querySelector( .document-editor__editable ) }, cloudServices: { // CKEditor Cloud Services 配置文档编辑器示例使用了评论、修订等协作能力。 // ... } } ) .then( editor { const toolbarContainer document.querySelector( .document-editor__toolbar ); toolbarContainer.appendChild( editor.ui.view.toolbar.element ); window.editor editor; } ) .catch( err { console.error( err ); } );注意必须等EditorUI#ready事件触发后再注入 UI。工具栏元素固定位于editor.ui.view.toolbar.element。示例代码 document-editor.js 还演示了更丰富的配置extraPlugins: [ TableColumnResize ]、完整的toolbar.itemsundo/redo、heading、bold/italic、link、insertImage、insertTable、mediaEmbed、列表与缩进、ui.viewportOffset等可作为文档编辑器的完整参考。2. 搭建 HTML 骨架编辑器只负责生成 UI 组件宿主结构由你定义。下面用两个容器分别承接工具栏与编辑区该结构在 document-editor.html 中即为最终示例的骨架div classdocument-editor div classdocument-editor__toolbar/div div classdocument-editor__editable-container div classdocument-editor__editable pThe initial editor data./p /div /div /div最外层div classdocument-editor虽非强制但推荐用来把整体包在一起。务必保证该 HTML 在编辑器创建时已存在于 DOM要么把引导代码放在 HTML 之后要么用DOMContentLoaded事件延迟执行 JavaScript。3. 编写样式让工具栏悬浮、让内容像一张纸以下 CSS 全部来自 document-editor.md 教程原文可直接复制使用。首先定义主容器为纵向 flex并设置垂直边界.document-editor { border: 1px solid var(--ck-color-base-border); border-radius: var(--ck-border-radius); /* 设置文档编辑器的垂直边界。 */ max-height: 700px; /* 用 flex 布局方便排布子区域。 */ display: flex; flex-flow: column nowrap; }让工具栏看起来悬浮在纸张上方.document-editor__toolbar { /* 保证工具栏容器始终在编辑区之上。 */ z-index: 1; /* 制造工具栏悬浮于编辑区的错觉。 */ box-shadow: 0 0 5px hsla( 0,0%,0%,.2 ); /* 使用 CKEditor 的 CSS 变量保持 UI 一致。 */ border-bottom: 1px solid var(--ck-color-toolbar-border); } /* 调整工具栏在容器内的观感。 */ .document-editor__toolbar .ck-toolbar { border: 0; border-radius: 0; }让编辑区看起来像一张居中于可滚动容器中的纸/* 让编辑区容器看起来像原生文字处理软件的内部。 */ .document-editor__editable-container { padding: calc( 2 * var(--ck-spacing-large) ); background: var(--ck-color-base-foreground); /* 使纸张内容可滚动。 */ overflow-y: scroll; } .document-editor__editable-container .ck-editor__editable { /* 设定纸张尺寸A4 比例。 */ width: 15.8cm; min-height: 21cm; /* 让纸张不贴住容器边缘。 */ padding: 1cm 2cm 2cm; border: 1px hsl( 0,0%,82.7% ) solid; border-radius: var(--ck-border-radius); background: white; /* 纸张投下轻微阴影3D 效果。 */ box-shadow: 0 0 5px hsla( 0,0%,0%,.1 ); /* 水平居中纸张。 */ margin: 0 auto; }最后定义内容区字体与标题/段落排版。官方建议使用.ck-content类来统一样式化编辑器内容标题、段落、列表等并让标题下拉预览与实际内容观感一致/* 设置纸张内容的默认字体。 */ .document-editor .ck-content, .document-editor .ck-heading-dropdown .ck-list .ck-button__label { font: 16px/1.6 Helvetica Neue, Helvetica, Arial, sans-serif; } /* 调整标题下拉容纳更大的标题样式。 */ .document-editor .ck-heading-dropdown .ck-list .ck-button__label { line-height: calc( 1.7 * var(--ck-line-height-base) * var(--ck-font-size-base) ); min-width: 6em; } /* 缩小标题下拉中的预览保持相对比例否则过大放不下。 */ .document-editor .ck-heading-dropdown .ck-list .ck-button:not(.ck-heading_paragraph) .ck-button__label { transform: scale(0.8); transform-origin: left; } /* 标题 1 */ .document-editor .ck-content h2, .document-editor .ck-heading-dropdown .ck-heading_heading1 .ck-button__label { font-size: 2.18em; font-weight: normal; } .document-editor .ck-content h2 { line-height: 1.37em; padding-top: .342em; margin-bottom: .142em; } /* 标题 2 */ .document-editor .ck-content h3, .document-editor .ck-heading-dropdown .ck-heading_heading2 .ck-button__label { font-size: 1.75em; font-weight: normal; color: hsl( 203, 100%, 50% ); } .document-editor .ck-heading-dropdown .ck-heading_heading2.ck-on .ck-button__label { color: var(--ck-color-list-button-on-text); } .document-editor .ck-content h3 { line-height: 1.86em; padding-top: .171em; margin-bottom: .357em; } /* 标题 3 */ .document-editor .ck-content h4, .document-editor .ck-heading-dropdown .ck-heading_heading3 .ck-button__label { font-size: 1.31em; font-weight: bold; } .document-editor .ck-content h4 { line-height: 1.24em; padding-top: .286em; margin-bottom: .952em; } /* 段落 */ .document-editor .ck-content p { font-size: 1em; line-height: 1.63em; padding-top: .5em; margin-bottom: 1.13em; } /* 引用块衬线字体 额外留白更显精致。 */ .document-editor .ck-content blockquote { font-family: Georgia, serif; margin-left: calc( 2 * var(--ck-spacing-large) ); margin-right: calc( 2 * var(--ck-spacing-large) ); }至此文档编辑器即可运行。教程还建议按需配置 HighlightConfig 高亮、FontSizeConfig 字号、FontFamilyConfig 字体 等特性以进一步提升体验。生命周期管理ready、destroy 与 DOM 清理ready 事件与注入时机编辑器初始化完成后会触发ready事件create()返回的 Promise resolve 时机相同。你应当在此之后访问editor.ui.view.toolbar.element并插入 DOM。若还需要菜单栏同样可在此后挂载editor.ui.view.menuBarView.element——手动测试 decouplededitor.ts 展示了三个容器菜单栏/工具栏/编辑区分别挂载的完整写法。destroy 与 DOM 清理关键差异与其他编辑器不同DecoupledEditor.destroy()不会自动移除工具栏和编辑区 DOM。源码 decouplededitor.ts 明确要求你在销毁链中自行清理editor.destroy() .then( () { // 从 DOM 移除工具栏。 editor.ui.view.toolbar.element.remove(); // 从 DOM 移除编辑区。 editor.ui.view.editable.element.remove(); console.log( Editor was destroyed ); } );手动测试 decouplededitor.manual.html 同样声明了该预期编辑器被销毁后UI 应保留在容器中只有.ck-body区域被移除。销毁过程中 decouplededitorui.ts 会先detachDomRoot再从引擎解绑编辑区最后销毁视图。updateSourceElementOnDestroy若编辑器创建在已有 DOM 元素之上且设置了updateSourceElementOnDestroy: true销毁时会把编辑器数据回写回该元素。销毁实现先缓存数据再销毁因为 model→view 转换在super.destroy()后不再可用随后调用updateSourceElement( data )见 decouplededitor.ts相关测试见 decouplededitor.js。测试与验证如何确认行为符合预期本包的自动化测试位于 tests/decouplededitor.js覆盖了上述绝大部分行为包括editorName标识、数据接口setData/getData存在性、默认根main的创建decouplededitor.jsUI 类实例化、工具栏自动分组的开关decouplededitor.jsaria-label的全部取值分支decouplededitor.js初始数据的所有冲突与归一化场景decouplededitor.jsroot.element三种形态及其在视图根上的映射decouplededitor.js异步数据初始化与根元素 limit 校验decouplededitor.js。在仓库中运行测试的命令为pnpm --filter ckeditor/ckeditor5-editor-decoupled test对应 package.json 中的test: vitest run脚本。小结DecoupledEditor 的精髓在于组件解耦、布局自定编辑器引擎、工具栏、菜单栏、编辑区各自独立UI 位置完全由集成方掌控。结合 document-editor 教程 的 HTML/CSS 手法你可以快速产出文档编辑器、沉浸式阅读编辑器等任意自定义布局同时完整保留 CKEditor 5 的功能集与无障碍支持如工具栏的键盘导航。更多编辑器类型对比可参考 CKEditor 5 编辑器类型编辑器生命周期可参考 编辑器生命周期获取与设置数据可参考 getting-and-setting-data。【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考