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...