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 配置源文件。从源码看,该模块的配置结构包含四个关键字段:
| 配置项 | 默认值 | 说明 |
|---|---|---|
stylePrefix | 'com-' | 命令相关 UI 的样式前缀 |
defaults | {} | 初始化时注册的默认命令集合 |
strict | true | 有状态命令(同时含run与stop)是否禁止重复执行;为true时若命令已激活,再次run不会触发run方法 |
defaultOptions | {} | 命令的默认选项,在命令执行/停止前与传入 options 合并,可用于统一注入公共行为 |
其中defaultOptions的典型用法是为某个命令统一注入选项,例如对core:component-drag设置skipGuidesRender等公共行为,其run/stop回调接收当前 options 并返回合并后的新 options。
初始化后动态注册(插件开发的标准方式)
绝大多数场景下命令是在编辑器初始化之后动态创建的——这也是开发 GrapesJS 插件时的推荐做法,此时需要使用 Commands API(即editor.Commands):
const 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 方法。
执行命令
调用命令使用runCommand:
editor.runCommand('my-command-id');::: tipeditor.runCommand是editor.Commands.run的别名;对应的editor.stopCommand则是editor.Commands.stop的别名。 :::
需要传递参数时,可以在第二个参数传入 options 对象:
editor.runCommand('my-command-id', { some: 'option' });在命令回调中,通过第三个参数接收这份 options(第二个参数sender表示是谁发起了命令请求,在上述场景中始终是editor):
commands.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)真正执行。也就是说,命令的执行入口在CommandAbstract(packages/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 是激活的命令 ID,value 是run方法最后一次的返回值。上面的例子返回undefined是因为没有返回值,是否返回、返回什么完全由你的实现决定。例如:
// 让 run 返回一些东西 run(editor) { alert('This command is now active'); return { activated: new Date(), }; } // 现在 getActive() 中该命令的 value 就是这个对象,而不再是 undefined停止有状态命令
停用命令使用editor.stopCommand:
editor.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', '<div>Some 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设为true:
const 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执行 stop | commands.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),仅供参考