You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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自定义构建流程,在构建阶段完成插件打包,避免运行时加载插件的上下文兼容问题:

  1. 进入你当初生成CKEditor自定义构建的项目目录(包含CKEditor构建配置、package.json的独立目录,不是Angular业务项目目录)
  2. 将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;
  1. 修改构建项目的入口src/ckeditor.js,将Placeholder加入内置插件列表,同时把placeholder加入默认工具栏配置,执行构建命令生成新的ckeditor.js。
  2. 用新生成的ckeditor.js替换Angular项目中src/ckCustomBuild目录下的旧文件。
  3. 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项目中运行时加载插件,需要完成以下修正:

  1. 在Angular项目中安装和你当前CKEditor构建版本完全一致的CKEditor相关依赖包(ckeditor5-core、ckeditor5-widget、ckeditor5-ui、ckeditor5-utils),将placeholder.plugin.js中的导入路径改为这些npm包,同时在文件末尾添加export default Placeholder。
  2. 修正组件配置:
    • 修正工具栏拼写错误,将Paceholder改为placeholder
    • 放开placeholderConfig配置
    • 将自定义构建内置的所有插件(你之前构建时选的bold、italic、link等全部插件)和Placeholder一起加入plugins数组,避免原有功能失效。

注意:该方案需要严格保证安装的CKEditor依赖包版本和预构建的ckeditor.js版本完全一致,否则会出现类实例判断失败、插件加载异常等很难排查的兼容问题。

内容的提问来源于stack exchange,提问作者Chris Kooken

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 02:15:38