Elementor web-cli 编辑器 $e.hooks API 全解:命令钩子的注册、触发机制与自定义开发规范
【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.项目地址: https://gitcode.com/GitHub_Trending/el/elementor
本文围绕 Elementor 仓库中 web-cli 编辑器 API 的文档 hooks.md 展开,系统讲解$e.hooks钩子管理器的完整 API、UI/Data 两类钩子的事件模型、基于源码实现的注册与触发流程,以及编写自定义 Hook 的命名、目录结构与组件集成规范。读完本文,你将能够在 Elementor 编辑器(或基于同一套$eAPI 的 web-cli 编辑器)中,正确地为任意命令挂载 before/after/catch/dependency 钩子,并理解其底层防递归、依赖中断(HookBreak)与容器类型分桶的实现原理。
一、$e.hooks 是什么:挂载在命令生命周期上的钩子管理器
根据官方文档描述,$e.hooksAPI 是$e.hooks.ui与$e.hooks.data的总管理器,允许开发者创建自定义钩子。这些钩子挂载在$e.commands上:每当$e.run()执行某个命令时,对应的钩子会在命令执行前(before)/后(after)/失败时(catch)被触发。
简而言之,$e.hooks把"命令"作为事件源,把"钩子"作为事件处理函数,形成一套确定性的命令扩展机制:
- UI 钩子:用于 UI/视图层操作(刷新菜单、切换样式、更新按钮状态等),不触碰 Elementor 的数据模型与历史栈;
- Data 钩子:用于对 Elementor 数据模型做自定义的数据操作,并可以创建"依赖"(dependency)——在命令执行前介入,甚至中断命令的执行。
文档中标注的实现位置为core/common/assets/js/api/core/hooks.js;从当前仓库的实际结构看,web-cli 编辑器 API 的实现位于 modules/web-cli/assets/js/core/hooks.js,其子管理器分别位于 modules/web-cli/assets/js/core/hooks/ui.js 与 modules/web-cli/assets/js/core/hooks/data.js,公共基类为 modules/web-cli/assets/js/core/hooks/base.js。下文的所有源码分析均基于这些文件。
二、API 速览:$e.hooks 的完整方法表
$e.hooks对外暴露两类方法:一类是通用的类型化方法(activate、deactivate、getAll、register、run),另一类是按"类型 + 事件"组合的便捷方法。完整方法签名如下(继承自文档 hooks.md):
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
$e.hooks.activate() | 无 | 无 | 激活所有钩子 |
$e.hooks.deactivate() | 无 | 无 | 停用所有钩子 |
$e.hooks.getAll() | 无 | {Array} | 获取所有已加载的钩子 |
$e.hooks.register() | {String}type,{String}event,{HookBase}instance | {Object}callback | 注册一个钩子 |
$e.hooks.run() | {String}type,{String}event,{String}command,{Object}args,{*}result | {Boolean} | 运行一个钩子 |
$e.hooks.registerDataAfter() | {HookBase}instance | {Object}callback | 注册命令执行后运行的 data 钩子 |
$e.hooks.registerDataCatch() | {HookBase}instance | {Object}callback | 注册命令失败时运行的 data 钩子 |
$e.hooks.registerDataDependency() | {HookBase}instance | {Object}callback | 注册作为依赖、在命令执行前运行的 data 钩子 |
$e.hooks.registerUIAfter() | {HookBase}instance | {Object}callback | 注册命令执行后运行的 UI 钩子 |
$e.hooks.registerUICatch() | {HookBase}instance | {Object}callback | 注册命令失败时运行的 UI 钩子 |
$e.hooks.registerUIBefore() | {HookBase}instance | {Object}callback | 注册命令执行前运行的 UI 钩子 |
$e.hooks.runDataAfter() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令后的 data 钩子 |
$e.hooks.runDataCatch() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令失败时的 data 钩子 |
$e.hooks.runDataDependency() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令前作为依赖的 data 钩子 |
$e.hooks.runUIAfter() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令后的 UI 钩子 |
$e.hooks.runUICatch() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令失败时的 UI 钩子 |
$e.hooks.runUIBefore() | {String}command,{Object}args,{*}result | {Boolean} | 运行命令前的 UI 钩子 |
从源码可以印证这张表的组织方式:modules/web-cli/assets/js/core/hooks.js#L8-L10 中,Hooks类直接持有两个子管理器实例:
export default class Hooks { data = new HooksData(); ui = new HooksUI(); // ... }- 通用方法
register(type, event, instance)与run(type, event, command, args, result)只是通过getType(type)找到对应子管理器后转发调用,见 hooks.js#L68-L87; - 12 个
registerXxx/runXxx便捷方法则是硬编码(type, event)组合后的转发,例如registerDataDependency()等价于register('data', 'dependency', instance)(见 hooks.js#L124-L126)。
这意味着:data类型的事件集是after / catch / dependency,而ui类型的事件集是after / catch / before,两类钩子的事件名并不对称,这是后文理解触发机制的关键。
三、两个子管理器:$e.hooks.ui 与 $e.hooks.data
3.1 $e.hooks.ui:不触碰数据模型的 UI 钩子
$e.hooks.ui管理UI钩子,允许你创建在命令before/after/catch时运行的自定义逻辑,但不影响 Elementor 的数据模型与历史(history)。钩子挂载在$e.commands上,每当命令运行时触发对应事件,主要用途是 UI/视图操作。
所有 UI 钩子都应继承位于modules/web-cli/assets/js/modules/hooks/ui/的基类(文档标注路径为core/common/assets/js/api/modules/hooks/ui/):
| 类 | 说明 |
|---|---|
$e.modules.hookUI.Base | 创建自定义 UI 钩子的"裸"基类 |
$e.modules.hookUI.After | 命令执行完成后运行 |
$e.modules.hookUI.Before | 命令执行前运行 |
$e.modules.hookUI.Catch | 命令失败时运行 |
3.2 $e.hooks.data:操作数据模型并支持依赖中断
$e.hooks.data管理Data钩子,允许你创建对 Elementor 数据模型的自定义数据操作,并创建依赖(dependency)。钩子同样挂载在$e.commands上,在由$e.run()执行的命令before/after/catch时被触发。所有 data 钩子应继承modules/web-cli/assets/js/modules/hooks/data/下的基类:
| 类 | 说明 |
|---|---|
$e.modules.hookData.Base | 创建自定义 data 钩子的"裸"基类 |
$e.modules.hookData.After | 命令执行完成后运行 |
$e.modules.hookData.Dependency | 命令执行前作为依赖运行(可中断命令) |
$e.modules.hookData.Catch | 命令失败时运行 |
Dependency 的特殊性:它是"命令中断型"钩子——
apply()必须返回布尔值,返回false表示中断命令执行,返回true表示继续。
更多细节可参阅 ui.md 与 data.md 两份子文档。
四、源码解析:HooksBase 的注册、分桶与触发流程
两个子管理器都继承自 hooks/base.js 中的HooksBase(该类还继承了模块基类Module)。理解基类的内部结构,就能完全解释 API 表背后的行为。
4.1 内部状态:callbacks 与 depth 双表
HooksBase构造函数初始化了两张核心表,见 base.js#L17-L55:
this.current = ''; // 当前正在执行的命令名 this.usedIds = []; // 已占用的钩子 id 列表 this.callbacks = { after: {}, // 事件 -> { 命令 -> { containerType|'all' -> [callback] } } catch: {}, }; this.depth = { after: {}, // 事件 -> { 钩子id -> 递归深度计数 } catch: {}, }; this.callbacksFlatList = {}; // id -> callback 的扁平索引,供 get(id) 使用两个子类型各自"补充"自己的事件桶:
Ui在构造时追加callbacks.before = {}与depth.before = {}(见 ui.js#L3-L14);Data在构造时追加callbacks.dependency = {}与depth.dependency = {}(见 data.js#L3-L14)。
这就解释了第二节的"事件不对称":事件名是否合法,取决于callbacks表里是否存在该键。基类的checkEvent()会直接检查Object.keys(this.callbacks),非法事件抛出`${type}: '${event}' is not available.`(见 base.js#L179-L183)。
4.2 注册流程:register 的三重校验
register(event, instance)依次执行三个校验,见 base.js#L236-L246:
checkEvent(event)— 事件必须是该类型支持的(ui: before/after/catch;data: dependency/after/catch);checkInstance(instance)— 实例的getType()必须与当前管理器类型一致,否则抛出invalid instance, please use: 'elementor-api/modules/hook-base.js'.;checkId(id)— 钩子 id 全局唯一,重复 id 抛出id: '${id}' is already in use.。
校验通过后进入registerCallback(),它从实例读取三个关键属性——instance.getCommand()、instance.getId()、instance.getContainerType()——并生成回调对象(见 base.js#L262-L305):
const callback = { id, callback: instance.run.bind( instance ), // 绑定实例自身的 run() isActive: true, activate() { this.isActive = true; }, deactivate() { this.isActive = false; }, };注册后,回调按容器类型分桶:如果getContainerType()返回了类型(如'document'、'section'),则存入callbacks[event][command][containerType];否则存入callbacks[event][command]['all']。这一"分桶"正是文档中约定"在已知容器类型时实现getContainerType()以提升性能"的底层原因——运行时只需遍历精确匹配的桶,而不必检查所有钩子。
4.3 触发流程:run 的完整调用链
run(event, command, args, result)的链路为:
run() -> getCallbacks(event, command, args) -> shouldRun() -> onRun() -> runCallbacks()getCallbacks()(见 base.js#L149-L170)从args中取出containers = [args.container],以第一个容器的type作为查询键,把"该容器类型的专属回调"与all桶的回调拼接起来;没有匹配时返回false。
run()(见 base.js#L319-L331)确认有回调后,记录this.current = command,调用onRun()钩子点(供子类埋 devTools 日志),再进入runCallbacks()。
runCallbacks()是防递归与错误隔离的核心(见 base.js#L344-L387),逐条回调处理:
!callback.isActive的回调被直接跳过——这就是$e.hooks.activate()/deactivate()能整体开关钩子的原因(它们遍历全部回调调用activate()/deactivate());- 深度计数防递归:执行前
this.depth[event][callback.id]++,仅当深度恰好为1时才真正执行onCallback()+runCallback(),执行完再--。从源码结构看,这保证了一个钩子在自身触发的嵌套命令链中不会被重复执行; - 错误隔离:回调返回 falsy 时抛出
Callback failed, event: '...';捕获到的异常中,若为$e.modules.HookBreak则原样向上重抛(依赖中断信号),其他错误仅通过Console.error(e)打印,不会中断命令主流程。
两个子类型对runCallback()的实现差异,决定了参数传递语义:
UI 钩子(ui.js#L16-L32):
switch ( event ) { case 'before': callback.callback( args ); // before:只传 args break; case 'catch': case 'after': callback.callback( args, result ); // after/catch:传 args 与结果/错误 break; default: return false; } return true;Data 钩子(data.js#L16-L45):
case 'dependency': { // 回调返回 false 且事件是 dependency 时,抛出 'Hook-Break' 中断 if ( ! callback.callback( args ) ) { this.depth[ event ][ callback.id ]--; throw new $e.modules.HookBreak; } return true; } case 'catch': case 'after': { // after 钩子"不可中断":即使回调返回负值,也要求返回正值, // 因为 runCallback 的返回值决定该回调是否"成功" return callback.callback( args, result ) || 'after' === event; }这里有两个值得注意的源码细节:
- dependency 的中断协议:
apply()返回false时,Data.runCallback手动将深度计数减回(因为抛出异常后基类的depth--语句不会执行),再抛出$e.modules.HookBreak;基类runCallbacks识别该异常后直接重抛,从而"安全地"中断整条命令执行链。这正是文档中"Dependency is a command-breaking hook"的实现依据; - data 钩子的额外前置条件:
Data重写了shouldRun(),见 data.js#L43-L45:
shouldRun( callbacks ) { return super.shouldRun( callbacks ) && elementor.documents.getCurrent().history.getActive(); }也就是说,data 钩子只有在当前文档的 history 处于激活状态时才会运行——从源码结构看,这是为了保证数据模型操作(以及由此产生的历史栈记录)只在编辑器正常可编辑上下文中生效,避免在预览等非编辑状态下产生副作用。
此外,两个子类型的onRun()/onCallback()都会在$e.devTools存在时输出回调运行日志(如 ui.js#L34-L48),为调试钩子执行顺序提供了内置支持。
五、编写自定义 Hook:模板、命名与完整示例
5.1 通用模板与命名约定
每个钩子文件都应遵循以下模板(继承自文档 hooks.md):
import HookUIAfter from 'elementor-api/modules/hooks/{TYPE}/after'; export class {FILE_NAME_CAMEL_CASE} extends HookUIAfter { getCommand() { return '{COMMAND}'; } getId() { return '{FILE_NAME_WITHOUT_JS}'; } getContainerType() { return '{CONTAINER_TYPE}'; } getConditions( args ) { return args.settings && 'undefined' !== typeof args.settings.post_status; } apply( args ) { const { footerSaver } = $e.components.get( 'document/save' ); footerSaver.setMenuItems( args.container.document ); footerSaver.refreshWpPreview(); } } export default {FILE_NAME_CAMEL_CASE};占位符的取值规范如下:
| 占位符 | 格式 - 说明 | 示例值 |
|---|---|---|
{TYPE} | 按钩子类型取ui或data | ui |
{COMMAND} | 要挂载的命令 | document/elements/settings |
{FILE_NAME} | kebab-case,文件名即描述"钩子做什么" | footer-saver-refresh-menu.js |
{FILE_NAME_CAMEL_CASE} | {FILE_NAME}的 camelCase 形式 | FooterSaverRefreshMenu |
{FILE_NAME_WITHOUT_JS} | {FILE_NAME}去掉.js后缀(用作钩子 id) | footer-saver-refresh-menu |
{FILE_PATH} | {TYPE}/{COMMAND}/{FILE_NAME} | ui/document/elements/settings/footer-saver-refresh-menu.js |
{CONTAINER_TYPE} | 可选;容器类型已知时提前声明,可提升性能 | document |
对应一个填好值的实例(挂载在document/elements/settings命令之后,负责刷新文档保存菜单):
// ui/document/elements/settings/footer-saver-refresh-menu.js import HookUIAfter from 'elementor-api/modules/hooks/ui/after'; export class FooterSaverRefreshMenu extends HookUIAfter { getCommand() { return 'document/elements/settings'; } getId() { return 'footer-saver-refresh-menu'; } getContainerType() { return 'document'; } getConditions( args ) { return args.settings && 'undefined' !== typeof args.settings.post_status; } apply( args ) { const { footerSaver } = $e.components.get( 'document/save' ); footerSaver.setMenuItems( args.container.document ); footerSaver.refreshWpPreview(); } } export default FooterSaverRefreshMenu;5.2 实战示例:UI 钩子(after)与 before 钩子
以下 after 型 UI 钩子示例完整继承自 ui.md,演示了在控制台动态注册并触发一个修改页面 DOM 的 UI 钩子(依赖文档 components.md 中示例 #1 注册的custom-component组件):
// 命令执行后触发:为页面所有 div 元素追加 CSS 类 class CustomUIHook extends $e.modules.hookUI.After { getCommand() { // 要监听的命令 return 'custom-component/example'; } getId() { // 钩子的唯一 id return 'custom-component-example-ui-hook'; } getConditions( args ) { // 钩子生效的条件 if ( args.toggleClass ) { return true; } return false; } /* * 实际的钩子逻辑。 */ apply( args, result ) { console.log( 'My hook custom logic', 'args: ', args, 'result: ', result ); // 为所有 div 元素添加 'custom-component' 类 document.querySelectorAll( 'div' ).forEach( ( element ) => element.classList.add( 'custom-component' ) ); } } // 将新钩子注册进 $e.hooks.ui const myHook = new CustomUIHook(); // 输出新钩子 console.log( myHook ); // 输出所有 after 型 ui 钩子 console.log( $e.hooks.ui.getAll().after ); // 触发测试 result = $e.run( 'custom-component/example', { toggleClass: true, } ); // 输出命令执行结果 console.log( 'e-hooks-ui-eg-1-result:', result );before 型钩子示例:在创建元素前,为section类型容器切换"整屏"样式类:
class CreateSectionIsFull extends $e.modules.hookUI.Before { getCommand() { return 'document/elements/create'; } getId() { return 'create-section-is-full'; } getConditions( args ) { const { containers = [ args.container ] } = args; return containers.some( ( /* Container */ container ) => 'section' === container.model.get( 'elType' ) ); } apply( args ) { const { containers = [ args.container ] } = args; containers.forEach( ( /* Container */ container ) => { if ( 'section' === container.model.get( 'elType' ) ) { container.view.toggleSectionIsFull(); } } ); } }5.3 实战示例:Data 钩子与依赖中断
after 型 data 钩子示例(继承自 data.md):
// 命令执行后触发的 data 钩子 class CustomDataHook extends $e.modules.hookData.After { getCommand() { // 要挂载的命令 return 'custom-component/example'; } getId() { // 钩子的唯一 id return 'custom-component-example-data-hook'; } // 可选但推荐的函数,用于优化。 // 如果容器类型已知,可在此提前声明: // //getContainerType() { // return 'container_type'; // 例如 section //} /* 可选函数:钩子运行的条件。 */ getConditions( args ) { return 'value' === args.property; } /* * 实际的钩子逻辑。 */ apply( args, result ) { console.log( 'My hook custom logic', 'args: ', args, 'containers: ', result ); } } const myHook = new CustomDataHook(); console.log( myHook ); console.log( $e.hooks.data.getAll().after ); result = $e.run( 'custom-component/example', { property: 'value', // 钩子生效的条件 } ); console.log( 'e-hooks-data-eg-1-result:', result );依赖(dependency)中断示例——当 section 的列数达到上限时,阻止继续创建列。注意其apply()必须返回布尔值,返回false即中断命令:
// 示例:列数达到上限时,阻止创建新列的钩子 class SectionColumnsLimit extends $e.modules.hookData.Dependency { getCommand() { return 'document/elements/create'; } getId() { return 'section-columns-limit'; } getContainerType() { return 'section'; } /* 注意:这是 Dependency 钩子,可中断——当 apply 返回 false 时中断 */ apply( args ) { const { containers = [ args.container ] } = args; // 若任一目标容器的列数已达上限,则中断命令 return ! containers.some( ( /**Container*/ container ) => { return container.view.isCollectionFilled(); } ); } }结合第四节源码,可以明确这条链路的完整行为:document/elements/create命令的 dependency 事件触发 ->SectionColumnsLimit.apply()返回false->Data.runCallback抛出HookBreak-> 基类重抛 -> 命令执行被安全中断,后续 after/catch 事件按框架约定处理。
六、组件集成规范:目录结构、index 聚合与 importHooks
文档 hooks.md 同时规定了钩子的工程组织规范,这是保证钩子可维护、可追踪的核心约定:
- 每个钩子归属一个组件(component),组件规范见 components.md;
- 组件可以重写
defaultHooks()方法来声明自己要导入的钩子; - 钩子通过内置方法
importHooks导入; - 所有钩子必须经由index 文件聚合导出,且必须在
component/hooks/index.js存在一个总 index 文件。
推荐的目录与文件结构如下(继承自原文档):
📦 component │ 📜 component.js │ └───📂 hooks │ 📜 index.js ( 导出全部钩子 ) │ │ └───📂 ui │ │ └───📂 document │ │ │ └───📂 elements │ │ │ │ └───📂 settings │ │ │ │ │ │ 📜 footer-saver-refresh-menu.js │ │ │ │ │ │ ... │ │ │ └───📂 save │ │ │ │ └───📂 set-is-modfifed │ │ │ │ │ │ 📜 update-button.js │ │ │ │ │ │ ... │ │ 📜 index.js ( 导出全部 ui 钩子 ) │ │ ... │ └──📂 data │ │ └───📂 document │ │ │ └───📂 elements │ │ │ │ └───📂 import │ │ │ │ │ │ 📜 bypass-import.js │ │ │ │ │ │ ... │ │ │ └───📂 save │ │ │ │ └───📂 save │ │ │ │ │ │ 📜 save-extras.js │ │ │ │ │ │ ... │ │ 📜 index.js ( 导出全部 data 钩子 ) │ │ ...各层 index 文件的写法:
component/hooks/index.js—— 汇聚两个类型:
export * from './ui/'; export * from './data/';component/hooks/ui/index.js—— 显式导出每个 ui 钩子:
export { FooterSaverRefreshMenu } from './document/elements/settings/footer-saver-refresh-menu'; export { UpdateButton } from './document/save/set-is-modifed/update-button';component/hooks/data/index.js—— 同理:
export { BypassImport } from './document/elements/import/bypass-import'; export { SaveExtras } from './document/save/save/save-extras';在component/hooks/下的任意层级可以有多少个 index 文件取决于你的组织习惯,唯一硬性要求是component/hooks/index.js必须存在并导出全部钩子。
最后在组件类中通过importHooks完成挂载(继承自原文档示例):
import * as hooks from './hooks/'; export class Component extends $e.modules.ComponentBase { getNamespace() { return 'component-name'; } defaultHooks() { return this.importHooks( hooks ); } }ComponentBase的实现可参考 modules/web-cli/assets/js/modules/component-base.js,钩子基类hook-base位于 modules/web-cli/assets/js/modules/hooks/ui/base.js 与 modules/web-cli/assets/js/modules/hooks/data/base.js 所在的模块目录中。
七、小结:关键机制与参考路径
从 API 表到源码实现,$e.hooks的核心机制可以归纳为:
- 事件模型不对称:UI 钩子支持
before/after/catch,Data 钩子支持dependency/after/catch;before只能用于 UI 层,dependency只能用于数据层,且只有 dependency 具备中断命令的能力(HookBreak); - 容器类型分桶:实现
getContainerType()后,钩子进入精确桶,运行时仅遍历"精确桶 + all 桶",是官方推荐的性能优化手段; - 防递归保护:
depth计数保证同一钩子在同一触发链中只执行一次; - 错误隔离:普通钩子异常仅
Console.error记录;HookBreak是唯一能"安全中断"命令的异常类型; - data 钩子的上下文的约束:当前文档 history 未激活时,
$e.hooks.data的回调不会运行; - 工程规范:钩子按
{TYPE}/{COMMAND}/{FILE_NAME}组织、kebab-case 命名、index 聚合导出、defaultHooks() + importHooks()挂载到组件。
深入阅读路径:
- 总文档:docs/modules/web-cli/assets/js/core/hooks.md
- UI 钩子:docs/modules/web-cli/assets/js/core/hooks/ui.md
- Data 钩子:docs/modules/web-cli/assets/js/core/hooks/data.md
- 组件规范:docs/modules/web-cli/assets/js/core/components.md
- 实现源码:modules/web-cli/assets/js/core/hooks.js、modules/web-cli/assets/js/core/hooks/base.js、modules/web-cli/assets/js/core/hooks/ui.js、modules/web-cli/assets/js/core/hooks/data.js
【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.项目地址: https://gitcode.com/GitHub_Trending/el/elementor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考