基於編輯器做二次開發,可能大部分的工作量都在於自定義插件
而 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...