Angular13集成CKEditor5自定义插件报plugincollection-plugin-not-found错误
故障原因
你遇到的plugincollection-plugin-not-found报错由以下几个问题共同导致:
- 插件依赖导入完全错误:
placeholder.plugin.js中所有CKEditor基础类(Plugin、Widget、Command等)都从打包后的ckeditor.js整体导入,这种方式拿到的是整个构建产物的导出对象,不是对应的单个类,继承生成的类不符合CKEditor插件校验规则。 - 插件未导出:
placeholder.plugin.js中仅定义了Placeholder相关类,没有做任何导出声明,外部import拿到的是空对象,和报错信息里的{"plugin":{}}完全对应。 - 工具栏配置拼写错误:工具栏配置项写的
Paceholder为拼写错误,正确的组件注册名是placeholder。 - 插件加载逻辑理解错误:预构建生成的
ckeditor.js是打包闭包产物,外部单独引入插件时从该文件拆分导入依赖,会出现模块上下文不一致、类实例判断失败的问题。 - 必要配置被注释:
placeholderConfig配置项被注释,插件初始化时读取不到占位符类型列表会触发运行时异常。 - 配置逻辑踩坑:CKEditor5的
config.plugins数组是全量覆盖逻辑,不是追加逻辑,只要传入该字段,编辑器只会加载数组内列出的插件,不会自动加载构建时内置的基础插件,会导致原有功能全部失效。
Angular环境下CKEditor5自定义插件正确集成方案
推荐方案(稳定性最高)
将自定义插件纳入CKEditor自定义构建流程,在构建阶段完成插件打包,避免运行时加载插件的上下文兼容问题:
- 进入你当初生成CKEditor自定义构建的项目目录(包含CKEditor构建配置、package.json的独立目录,不是Angular业务项目目录)
- 将
placeholder.plugin.js放到构建项目的src/plugins目录下,修正文件内的依赖导入和导出逻辑,修正后代码如下:
// 从对应CKEditor npm包导入依赖,不要从打包后的ckeditor.js导入 import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import Command from '@ckeditor/ckeditor5-core/src/command'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import { viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget/src/utils'; import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; class Placeholder extends Plugin { static get requires() { return [PlaceholderEditing, PlaceholderUI]; } } class PlaceholderCommand extends Command { execute({ value }) { const editor = this.editor; const selection = editor.model.document.selection; editor.model.change((writer) => { const placeholder = writer.createElement('placeholder', { ...Object.fromEntries(selection.getAttributes()), name: value, }); editor.model.insertContent(placeholder); writer.setSelection(placeholder, 'on'); }); } refresh() { const model = this.editor.model; const selection = model.document.selection; const isAllowed = model.schema.checkChild(selection.focus.parent, 'placeholder'); this.isEnabled = isAllowed; } } class PlaceholderUI extends Plugin { init() { const editor = this.editor; const t = editor.t; const placeholderNames = editor.config.get('placeholderConfig.types'); editor.ui.componentFactory.add('placeholder', (locale) => { const dropdownView = createDropdown(locale); addListToDropdown(dropdownView, getDropdownItemsDefinitions(placeholderNames)); dropdownView.buttonView.set({ label: t('Placeholder'), tooltip: true, withText: true, }); const command = editor.commands.get('placeholder'); dropdownView.bind('isEnabled').to(command); this.listenTo(dropdownView, 'execute', (evt) => { editor.execute('placeholder', { value: evt.source.commandParam }); editor.editing.view.focus(); }); return dropdownView; }); } } function getDropdownItemsDefinitions(placeholderNames) { const itemDefinitions = new Collection(); for (const name of placeholderNames) { const definition = { type: 'button', model: new Model({ commandParam: name, label: name, withText: true, }), }; itemDefinitions.add(definition); } return itemDefinitions; } class PlaceholderEditing extends Plugin { static get requires() { return [Widget]; } init() { this._defineSchema(); this._defineConverters(); this.editor.commands.add('placeholder', new PlaceholderCommand(this.editor)); this.editor.editing.mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) => viewElement.hasClass('placeholder')) ); this.editor.config.define('placeholderConfig', { types: ['date', 'first name', 'surname'], }); } _defineSchema() { const schema = this.editor.model.schema; schema.register('placeholder', { allowWhere: '$text', isInline: true, isObject: true, allowAttributesOf: '$text', allowAttributes: ['name'], }); } _defineConverters() { const conversion = this.editor.conversion; conversion.for('upcast').elementToElement({ view: { name: 'span', classes: ['placeholder'], }, model: (viewElement, { writer: modelWriter }) => { const name = viewElement.getChild(0).data.slice(1, -1); return modelWriter.createElement('placeholder', { name }); }, }); conversion.for('editingDowncast').elementToElement({ model: 'placeholder', view: (modelItem, { writer: viewWriter }) => { const widgetElement = createPlaceholderView(modelItem, viewWriter); return toWidget(widgetElement, viewWriter); }, }); conversion.for('dataDowncast').elementToElement({ model: 'placeholder', view: (modelItem, { writer: viewWriter }) => createPlaceholderView(modelItem, viewWriter), }); function createPlaceholderView(modelItem, viewWriter) { const name = modelItem.getAttribute('name'); const placeholderView = viewWriter.createContainerElement('span', { class: 'placeholder', }); const innerText = viewWriter.createText('{' + name + '}'); viewWriter.insert(viewWriter.createPositionAt(placeholderView, 0), innerText); return placeholderView; } } } // 最后正确导出插件 export default Placeholder;
- 修改构建项目的入口
src/ckeditor.js,将Placeholder加入内置插件列表,同时把placeholder加入默认工具栏配置,执行构建命令生成新的ckeditor.js。 - 用新生成的
ckeditor.js替换Angular项目中src/ckCustomBuild目录下的旧文件。 - Angular业务组件中无需单独引入Placeholder插件,也不需要在配置中传入
plugins数组,直接使用构建好的编辑器实例即可,配置参考如下:
import * as customBuild from 'src/ckCustomBuild/ckeditor'; @Component({ selector: 'bxl-abstract-management-configuration', templateUrl: 'abstract-management-configuration.component.html', }) export class AbstractManagementConfigurationComponent implements OnInit { public Editor = customBuild; htmlEditorConfig = { toolbar: { // 修正拼写,加入placeholder项 items: ['bold', 'italic', 'underline', 'link', 'alignment', 'bulletedList', 'numberedList', '|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', '|', 'placeholder'], shouldNotGroupWhenFull: false, }, // 放开配置注释 placeholderConfig: { types: ['date', 'color', 'first name', 'surname'], }, fontSize: { options: [9, 11, 13, 'default', 17, 19, 21], }, mediaEmbed: { previewsInData: true, }, }; // 其余业务逻辑不变 }
运行时加载插件方案(不推荐,易出现版本兼容问题)
如果不想重新构建CKEditor,选择在Angular项目中运行时加载插件,需要完成以下修正:
- 在Angular项目中安装和你当前CKEditor构建版本完全一致的CKEditor相关依赖包(ckeditor5-core、ckeditor5-widget、ckeditor5-ui、ckeditor5-utils),将
placeholder.plugin.js中的导入路径改为这些npm包,同时在文件末尾添加export default Placeholder。 - 修正组件配置:
- 修正工具栏拼写错误,将
Paceholder改为placeholder - 放开
placeholderConfig配置 - 将自定义构建内置的所有插件(你之前构建时选的bold、italic、link等全部插件)和Placeholder一起加入
plugins数组,避免原有功能失效。
- 修正工具栏拼写错误,将
注意:该方案需要严格保证安装的CKEditor依赖包版本和预构建的ckeditor.js版本完全一致,否则会出现类实例判断失败、插件加载异常等很难排查的兼容问题。
内容的提问来源于stack exchange,提问作者Chris Kooken
相关产品推荐
相关产品推荐

