ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Elementor web-cli 编辑器 $e.hooks API 全解:命令钩子的注册、触发机制与自定义开发规范

Elementor web-cli 编辑器 $e.hooks API 全解:命令钩子的注册、触发机制与自定义开发规范 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.data3.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或dataui{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.mdUI 钩子:docs/modules/web-cli/assets/js/core/hooks/ui.mdData 钩子: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),仅供参考
返回列表