GrapesJS Commands 模块完全指南:命令的注册、状态管理、扩展与事件拦截
GrapesJS Commands 模块完全指南命令的注册、状态管理、扩展与事件拦截【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs导读GrapesJS 的 Commands命令模块是整个编辑器功能调用的中枢它把散落在各处的功能画布清空、组件选中、预览、全屏、打开面板等统一抽象为一个个可复用的命令并在此基础上提供了状态追踪、覆盖扩展与事件拦截能力。本文以 docs/modules/Commands.md 为主线结合 packages/core/src/commands 下的源码实现带你掌握命令的定义方式、默认命令清单、有状态命令run/stop机制、命令扩展extend以及基于事件的命令流程拦截读完即可在自己的插件或业务集成中规范地使用 Commands 模块。本文适用于 GrapesJS v0.14.61 及以上版本当前仓库对应packages/core中的实现。命令的基本概念与配置在 GrapesJS 中一个最基础的命令就是一个普通函数但 Commands 模块的真正价值在于集中化管理它让功能可以被统一追踪、复用、扩展甚至在某些条件下被中断。这也是官方文档将其定位为功能入口点的原因——所有可复用的逻辑都应优先沉淀为命令。初始化时通过配置注册你可以在初始化编辑器时通过commands.defaults选项直接声明命令。注意此时id与run是必填项const editor grapesjs.init({ // ... commands: { defaults: [ { // id 和 run 在这种用法下是必填的 id: my-command-id, run() { alert(This is my command); }, }, { id: ..., // ... } ], } });其他可用的配置项可以直接查看 commands 配置源文件。从源码看该模块的配置结构包含四个关键字段配置项默认值说明stylePrefixcom-命令相关 UI 的样式前缀defaults{}初始化时注册的默认命令集合stricttrue有状态命令同时含run与stop是否禁止重复执行为true时若命令已激活再次run不会触发run方法defaultOptions{}命令的默认选项在命令执行/停止前与传入 options 合并可用于统一注入公共行为其中defaultOptions的典型用法是为某个命令统一注入选项例如对core:component-drag设置skipGuidesRender等公共行为其run/stop回调接收当前 options 并返回合并后的新 options。初始化后动态注册插件开发的标准方式绝大多数场景下命令是在编辑器初始化之后动态创建的——这也是开发 GrapesJS 插件时的推荐做法此时需要使用 Commands API即editor.Commandsconst commands editor.Commands; commands.add(my-command-id, (editor) { alert(This is my command); }); // 或者等价地写成对象形式 commands.add(my-command-id, { run(editor) { alert(This is my command); }, });可以看到定义命令非常简单只需提供一个 ID 和一个回调函数。回调的第一个参数是 Editor 实例因此你可以访问任何其他模块或 API 方法。执行命令调用命令使用runCommandeditor.runCommand(my-command-id);::: tipeditor.runCommand是editor.Commands.run的别名对应的editor.stopCommand则是editor.Commands.stop的别名。 :::需要传递参数时可以在第二个参数传入 options 对象editor.runCommand(my-command-id, { some: option });在命令回调中通过第三个参数接收这份 options第二个参数sender表示是谁发起了命令请求在上述场景中始终是editorcommands.add(my-command-id, (editor, sender, options {}) { alert(This is my command ${options.some}); });从源码层面看runCommand最终会走到CommandsModule.runCommand见 packages/core/src/commands/index.ts其执行逻辑是若命令尚未激活、或传入了force、或config.strict为 false则调用command.callRun(editor, options)真正执行。也就是说命令的执行入口在CommandAbstractpackages/core/src/commands/view/CommandAbstract.ts中完成统一的事件触发与状态登记详见下文事件一节。到目前为止命令看起来还只是一个公共函数入口但它的真正优势在后面的有状态命令、扩展和事件拦截中才会完全体现。内置默认命令一览GrapesJS 内置了一组默认命令你可以通过editor.Commands.getAll()获取当前所有可用命令的对象包括后续插件添加的命令。默认命令以core:*命名空间标识官方也建议自定义命令使用命名空间。以下是主要的内置命令core:canvas-clear—— 清空画布中的全部内容HTML 与 CSS。对应实现见 CanvasClear.ts其核心逻辑只有两行ed.Components.clear()与ed.Css.clear()core:component-delete—— 删除选中的组件见 ComponentDelete.tscore:component-enter—— 选中当前选中组件的第一个子组件见 ComponentEnter.tscore:component-exit—— 选中当前组件的父级组件见 ComponentExit.tscore:component-next—— 选中下一个兄弟组件见 ComponentNext.tscore:component-prev—— 选中上一个兄弟组件见 ComponentPrev.tscore:component-outline—— 开启组件外轮廓边框见 SwitchVisibility.tscore:component-offset—— 显示组件的偏移信息margin、padding见 ShowOffset.tscore:component-select—— 启用画布中组件的选择流程见 SelectComponent.tscore:copy—— 复制当前选中的组件见 CopyComponent.tscore:paste—— 粘贴复制的组件见 PasteComponent.tscore:preview—— 在画布中预览模板效果见 Preview.tscore:fullscreen—— 让编辑器进入全屏见 Fullscreen.tscore:open-code—— 打开一个展示模板代码的默认面板见 ExportTemplate.tscore:open-layers—— 打开图层面板见 OpenLayers.tscore:open-styles—— 打开样式管理器面板见 OpenStyleManager.tscore:open-traits—— 打开属性Trait面板见 OpenTraitManager.tscore:open-blocks—— 打开块Blocks面板见 OpenBlocks.tscore:open-assets—— 打开资源Assets面板见 OpenAssets.tscore:undo—— 执行撤销操作内部调用e.UndoManager.undo()core:redo—— 执行重做操作内部调用e.UndoManager.redo()除上述命令外从 packages/core/src/commands/index.ts 的commandsDef列表中还可以看到core:resize、core:canvas-move、core:component-move、core:component-style-clear、core:component-drag等命令以及内部使用的tlb-delete、tlb-clone、tlb-move工具栏删除/克隆/移动等命令。此外源码中为部分命令保留了旧版名称的别名映射如open-sm、open-tm、select-comp、sw-visibility、show-offset、move-comp、select-parent、export-template、preview、resize、fullscreen等并会在运行旧名称命令时将事件转发到对应的core:*命名方便老版本迁移。有状态命令Stateful Commands前面提到命令执行完不会留下任何痕迹。但在某些场景下我们需要追踪命令的执行状态。GrapesJS 默认支持这一点只要把命令声明为包含run和stop两个方法的对象即可commands.add(my-command-state, { run(editor) { alert(This command is now active); }, stop(editor) { alert(This command is disabled); }, });此时执行editor.runCommand(my-command-state)命令会被登记为激活状态。查询状态可以用commands.isActive(my-command-state)—— 返回布尔值判断该命令是否激活commands.getActive()—— 返回所有激活命令的对象例如{ ... my-command-state: undefined }这个对象中key 是激活的命令 IDvalue 是run方法最后一次的返回值。上面的例子返回undefined是因为没有返回值是否返回、返回什么完全由你的实现决定。例如// 让 run 返回一些东西 run(editor) { alert(This command is now active); return { activated: new Date(), }; } // 现在 getActive() 中该命令的 value 就是这个对象而不再是 undefined停止有状态命令停用命令使用editor.stopCommandeditor.stopCommand(my-command-state);和runCommand一样你可以把 options 作为第二个参数传入并在stop方法中使用它。重复执行与 force 选项关键行为命令激活期间再次runCommand不会触发run方法。这可以防止激活流程被重复执行而导致状态不一致例如一个计数器run时加一、stop时减一。如果你需要一条命令重复执行多次那它大概率不该是有状态命令即不要定义stop方法但如果你确定自己的应用状态没问题也可以强制执行editor.runCommand(my-command-state, { force: true });同样的逻辑也适用于stopCommand命令未激活时直接stop不会生效除非传入force: true。这一行为与配置项strict默认true直接对应在 config.ts 与runCommand/stopCommand的源码判断中可以看到只有命令已激活stop 时或未激活run 时、或options.force为真、或config.strict为假三者满足其一实际的run/stop才会被调用。有状态命令与 UI 的一致性陷阱::: danger 警告 如果你的有状态命令涉及 UI务必保持 UI 状态与命令逻辑状态一致。 :::以 Modal模态框作为命令状态的指示器为例commands.add(my-command-modal, { run(editor) { editor.Modal.open({ title: Modal example, content: My content, }); }, stop(editor) { editor.Modal.close(); }, });如果运行该命令后手动关闭模态框例如点击右上角的 x再运行它会发现模态框不再打开。原因在于命令仍然是激活状态你可以在commands.getActive()中看到它而run在激活状态下不会再次执行。解决办法是在模态框关闭时同步停用命令。run(editor) { editor.Modal.open({ title: Modal example, content: My content, }).onceClose(() this.stopCommand()); }上面的示例用到了 Modal 模块的辅助方法onceClose关闭后回调以及命令自身的stopCommand方法。stopCommand在 CommandAbstract.ts 中实现其内部等价于调用Commands.stop(this.id, opts)。当然具体 UI 的同步逻辑会因你的需求而异但原则一致UI 生命周期结束时应同步终止对应的有状态命令。这种关闭即停用的模式在官方内置命令中也很常见例如core:open-code见 ExportTemplate.ts在打开代码展示模态框后会通过modal.getModel().once(change:open, () editor.stopCommand(...))在模态框关闭时自动停止命令core:preview见 Preview.ts的run中会创建退出预览辅助元素并绑定stopCommand同时在其stop中恢复面板可见性、还原画布样式与选中项。覆盖与扩展命令命令的另一个巨大优势是可以被轻松覆盖或扩展。覆盖Overwrite先注册一个命令commands.add(my-command-1, (editor) { alert(This is command 1); });需要覆盖时直接以相同 ID 重新添加即可commands.add(my-command-1, (editor) { alert(This is command 1 overwritten); });扩展Extend当命令以对象形式定义并包含辅助方法时可以用extend方法做局部增强。先定义基础命令commands.add(my-command-2, { someFunction1() { alert(This is function 1); }, someFunction2() { alert(This is function 2); }, run() { this.someFunction1(); this.someFunction2(); }, });然后通过传入命令 ID 进行扩展只覆盖需要改变的方法未提供的方法会从原命令原型继承commands.extend(my-command-2, { someFunction2() { alert(This is function 2 extended); }, });从 index.ts 的extend实现可以看出它会取出原命令构造函数的原型对象与新传入的对象合并后再通过add重新注册同时若被扩展的是带旧版名称的core:*命令还会同步扩展旧名称别名对应的命令。需要说明的是extend要求被扩展的命令以对象形式定义源码注释明确写道 The command to extend should be defined as an object纯函数形式的命令没有可继承的方法集合。事件拦截与中断命令流程Commands 模块还提供了一组事件可用于在命令执行流程中注入额外逻辑甚至中断命令。监听 run 与 stop以之前创建的my-command-modal为例可以监听以下事件editor.on(command:run:my-command-modal, () { console.log(After my-command-modal execution); // 例如向模态框追加额外内容 const modalContent editor.Modal.getContentEl(); modalContent.insertAdjacentHTML(beforeEnd, divSome content/div); }); editor.on(command:run:before:my-command-modal, () { console.log(Before my-command-modal execution); }); // 有状态命令的 stop 事件 editor.on(command:stop:my-command-modal, () { console.log(After my-command-modal is stopped); }); editor.on(command:stop:before:my-command-modal, () { console.log(Before my-command-modal is stopped); });如果你需要监听所有命令可以省略命令 ID 部分editor.on(command:run, (commandId) { console.log(Run, commandId); }); editor.on(command:stop, (commandId) { console.log(Stop, commandId); });源码视角事件的实际触发顺序结合 CommandAbstract.ts 中的callRun/callStop可以完整还原一次命令执行的事件链路执行callRun触发command:run:before:{ID}回调参数为{ options }若options.abort为真触发command:abort:{ID}并直接返回命令不执行调用this.run(editor, sender, options)得到result若非无状态命令存在stop将result写入Commands.active[id]登记激活状态依次触发command:run:{ID}、command:call:{ID}type: run、command:run全局、command:call全局停止callStop触发command:stop:before:{ID}回调参数为{ options }调用this.stop(editor, sender, options)得到result从Commands.active中删除该命令解除激活状态依次触发command:stop:{ID}、command:call:{ID}type: stop、command:stop全局、command:call全局这些事件名在 types.ts 中以枚举形式定义command:run、command:run:、command:run:before:、command:abort:、command:stop:、command:stop:before:、command:call、command:call:完整的事件清单也可在 Commands API 文档 中查阅例如command:run—— 任意命令执行后触发参数{ id, result, options }command:run:COMMAND-ID—— 指定命令执行后触发参数{ result, options }command:run:before:COMMAND-ID—— 命令执行前触发参数{ options }command:abort:COMMAND-ID—— 命令执行被中断时触发参数{ options }command:stop/command:stop:COMMAND-ID/command:stop:before:COMMAND-ID—— 对应停止流程command:call/command:call:COMMAND-ID—— 无论 run 还是 stop 都会触发参数额外包含type: run | stop中断命令执行有时需要基于某些条件阻止一个已存在的命令执行。此时应在command:run:before:{COMMAND-ID}事件中把options.abort设为trueconst condition 1; editor.on(command:run:before:my-command-modal, (options) { if (condition) { options.abort true; console.log(Prevent my-command-modal from execution); } });结合上面的源码链路可知一旦abort被置真callRun会触发command:abort:{ID}并跳过run方法命令不会执行也不会登记激活状态。这个机制非常适合做权限控制、编辑态校验或前置条件检查。Commands API 方法速查除了上文已涉及的add、extend、run/runCommand、stop/stopCommand、isActive、getActive、getAll之外editor.Commands还提供以下常用方法完整签名见 docs/api/commands.md方法作用示例remove(id)从集合中移除命令若该命令当前处于激活状态会先以force: true执行 stopcommands.remove(my-command)get(id)按 ID 获取命令对象const cmd commands.get(myCommand); cmd.run();has(id)判断命令是否存在commands.has(core:undo) // true另外编辑器层面还有一组与命令相关的辅助方法editor.runCommand(id, options)、editor.stopCommand(id, options)前两者即Commands.run/stop的别名以及源码中用于维护默认命令状态的runDefault/stopDefault见 Editor.ts。runDefault会读取编辑器配置中的defaultCommand例如core:component-select在需要时停止并重新运行该默认命令以保证画布交互模式选择/拖拽与当前操作状态一致——这也是core:preview在进入预览时会调用stopDefault、退出时调用runDefault的原因。结语Commands 模块的设计看似简单但正确使用会非常强大。如果你正在为 GrapesJS 开发插件请尽可能用命令来封装功能为每个可复用的逻辑赋予命名空间化的 ID如my-plugin:do-something通过run/stop维护状态利用事件做流程扩展与中断再配合extend复用既有命令的能力。这套机制带来的可复用性与可控性会让你的插件逻辑更清晰、更易维护也更容易与编辑器内置功能协同工作。【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考