Talk is cheap. Show me your code

CKEditor 5 摸爬滚打(二)—— 自定义一个简单的加粗插件(上)

基于编辑器做二次开发,可能大部分的工作量都在于自定义插件

而 CKEditor 5 实现了一套自己的 MVC 架构,导致开发自定义插件尤为复杂

 

一、插件的基本架构

CKEditor 5 的自定义插件都需要从 Plugin 类继承,在此基础上根据实际情况开发三个模块:

1. editing: 插件的核心代码,注册插件对应的 Model,以及插件相关的命令、视图转换等;

2. ui: 常用的是 ButtonView,用来注册工具栏上的图标按钮;其他需要自定义的视图需要自行编写模板 Template

3. command: 自定义指令 Command,一般用于工具栏,用来控制工具栏按钮的状态和行为;也可以注册一般命令,只在代码中触发,而不暴露给用户;

 

这里提到了 Model 这个概念,它是 CKEditor 5 在编辑器中的数据模型

也就是说,我们在 CKEditor 5 中编辑内容的时候,并不是像常规编辑器那样直接编辑 DOM,而是编辑 Model

Model 其实就像是 Vue 或者 React 中的模板,每个插件需要创建自己的 schema(类似于组件),组装成类似这样的 Model

<$root>
  <paragraph>
    <$text>this is text content</$text>
  </paragraph>
  <paragraph>
    <plugin-image src="foo" title="bar"></plugin-image>
  </paragraph>
</$root>

然后通过转换器 conversion,在输出的时候将 Model 转换为富文本:

<p>this is text content</p>
<p>
  <div class="plugin-image">
    <img src="foo">
    <p class="title">bar</p>
  </div>
</p>

这里 plugin-image 的转换结果是瞎写的,在开发的时候需要自行定义转换规则

需要注意的是,由于 Model 和 conversion 的存在,一切直接操作 DOM 的开发手段都会失效

 

一下子接收到这些概念可能有点懵,不要慌,接下来用一个加粗插件的简单例子来深入了解

 

 

二、添加工具栏图标

在项目的 packages 目录下创建插件目录 plugin-bold,然后创建以下文件:


首先是 command.js:

// command.js

import Command from "@ckeditor/ckeditor5-core/src/command";

export default class BoldCommand extends Command {
  refresh() {
    this.isEnabled = true;
  }

  execute() {
    console.log("Execute Plugin-Bold");
  }
}

这里的 BoldCommand 对象继承自 CKEditor5-Core 的 Command 类,这个类提供了三个静态属性:

1. editor: 编辑器实例;

2. value: 命令的值,有需要时可以手动修改;

3. isEnabled: 是否启用,命令被禁用时无法触发,一般会关联工具栏中的启用状态。

另外还有两个钩子函数 refresh() 和 execute()

refresh 会在编辑器更新的时候执行,类似于 React 中的 render 函数

execute 是该命令的执行函数,会在命令被触发时执行

目前只是简单的创建了 BoldCommand 这个子类,具体的逻辑后面再来开发 


然后编辑 editing.js

// editing.js

import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
import BoldCommand from "./command";
import { COMMAND_NAME__BOLD } from "./constant";

export default class BoldEditing extends Plugin {
  static get pluginName() {
    return "BoldEditing";
  }
  init() {
    const editor = this.editor;
    // 注册一个 BoldCommand 命令
    editor.commands.add(COMMAND_NAME__BOLD, new BoldCommand(editor));
  }
}

eidting.js 继承自 Plugin 类,加载的时候会自动执行 init() 方法

完整的 editing.js 会包含很多内容,这里先只是注册一个 BoldCommand 命令,其他的逻辑后面补充

在通过 editor.commands.add() 方法注册命令的时候,第一个参数是命令名称,类型为字符串,我放在 constant.js 中单独维护

// constant.js
export const COMMAND_NAME__BOLD = 'ck-bold';
export const COMMAND_LABEL__BOLD = '加粗';

接下来就是加粗插件在工具栏上的按钮 toolbar-ui.js

// toolbar-ui.js

import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
import ButtonView from "@ckeditor/ckeditor5-ui/src/button/buttonview";
import boldIcon from "@ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg";
import { COMMAND_NAME__BOLD, COMMAND_LABEL__BOLD } from "./constant";

export default class BoldToolbarUI extends Plugin {
  init() {
    this._createToolbarButton();
  }

  _createToolbarButton() {
    const editor = this.editor;
    const command = editor.commands.get(COMMAND_NAME__BOLD);
    editor.ui.componentFactory.add(COMMAND_NAME__BOLD, (locale) => {
      const view = new ButtonView(locale);
      view.set({
        label: COMMAND_LABEL__BOLD,
        tooltip: true,
        icon: boldIcon,
        // withText: true, // 在按钮上展示 label
        class: "toolbar_button_bold",
      });
      // 将按钮的状态关联到命令对应值上
      view.bind("isOn", "isEnabled").to(command, "value", "isEnabled");
      // 点击按钮时触发相应命令
      this.listenTo(view, "execute", () => editor.execute(COMMAND_NAME__BOLD));
      return view;
    });
  }
}

这里主要是引入了 ButtonView,并基于此创建了一个按钮实例 view(属性的注释可以参考第一小节的思维导图)

然后通过 bind() 方法将按钮 view 的 isOn 状态关联到 command 命令的值 value,将 isEnabled 状态关联到命令的 isEnabled

最终通过 editor.ui.componentFactory.add() 方法创建了一个 UI 组件,该方法的第一个参数是组件名称

因为没必要创建多余变量,我直接用了命令名称 COMMAND_NAME__BOLD(也可以用别的名称,但也需要单独维护,因为后面还会用到)

创建 UI 组件之后,就可以在创建组件的时候,通过配置 toolbar 属性(在数组中添加刚才设置的组件名称)将对应的按钮展示到 toolbar 上

在 toolbar 上展示的按钮,可以通过按钮自身的 isOn 和 isEnabled 状态来高亮和禁用

另外,BoldToolbarUI 依然是继承自 Plugin 类 

 

 

三、在编辑器中引入插件

插件的三大模块已经搞定,接下来在 main.js 中引入

// plugin-bold/main.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ToolbarUI from './toolbar-ui';
import BoldEditing from './editing';
import { COMMAND_NAME__BOLD } from './constant';

export default class Bold extends Plugin {
  static get requires() {
    return [ BoldEditing, ToolbarUI ];
  }
  static get pluginName() {
   return COMMAND_NAME__BOLD;
  }
}

main.js 中的 Bold 也是 Plugin 的子类,这里有一个静态方法 requires,这个方法返回一个由 Plugin 组成的数组,用于加载依赖插件

到此为止这个插件已经可以用了,在编辑器 packages/my-editor/src/index.js 中注释掉除了 Essentials 和 Paragraph 以外的插件

并调整 create 函数中的 plugins 和 toolbar 配置项,删除被注释掉的插件

然后引入自己开发的 plugin-bold 插件

import Bold from '../../plugin-bold/main';

后面还会引入更多的自定义插件,所以最好是添加路径别名,可以在根目录的 webpack.config.js 追加配置项:

resolve: {
  alias: {
    "@plugin": path.resolve("/packages"),
  },
}

打包配置文件 packages/my-editor/webpack.config.js 同样需要修改:

resolve: {
  alias: {
    "@plugin": path.resolve( __dirname, "../"),
  },
},

然后就能用别名引入插件了:

import Bold from "@plugin/plugin-bold/main";

如果使用 VSCode 无法识别路径,可以在项目的根目录添加一个 jsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@plugin/*": ["packages/*"]
    }
  },
  "exclude": ["node_modules"]
}

回到编辑器文件 packages/my-editor/src/index.js,编辑 ClassicEditor.create() 方法的第二个参数中的 plugins 和 toolbar

plugins: [ Essentials, Paragraph, Bold ],
toolbar: [ "undo", "redo", "|", Bold.pluginName ],

这里 toolbar 中添加的是 toolbar-ui.js 文件中 editor.ui.componentFactory.add() 创建的 UI 组件名

插件已经引入了,运行 yarn run dev 启动项目,可以看到这样的编辑器:

点击工具栏上的加粗按钮,控制台会打印 "Execute Plugin-Bold",这说明我们成功地迈出了自定义插件的第一步

接下来就是最为头疼的部分:model 与 conversion

to be continue...

posted @ 2021-02-26 16:54  Wise.Wrong  阅读(5175)  评论(0编辑  收藏  举报