Elementor Editor V2 编辑器包扩展实战:从 PHP 注册、UI 注入到 MCP 工具挂载
【免费下载链接】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 开源仓库中的 add-editor-package 示例文档,完整讲解如何通过"插件自有 Package"扩展 Editor V2 编辑器:先用 PHP 过滤器注册包名,再在 TypeScript 包的init()中注入 UI 槽位、注册面板与菜单,最后通过@elementor/editor-mcp为编辑器内 Agent(Angie / WebMCP)注册领域化 MCP 工具。读完本文,你将掌握一条可复制的端到端扩展路径,并能对照 site-navigation 模块 等仓库内真实实现进行验证。
一、前置认知:Editor V2 的包加载机制
在动手写代码之前,需要先理解"包(Package)"在 Editor V2 架构中的位置。Editor V2 是 Elementor 模块化的编辑器前端:功能以独立构建的包(React + TypeScript)为单位发布,通过 WordPress 脚本句柄(script handle)入队,并暴露在window.elementorV2全局对象上。PHP 端通过 WordPress 过滤器把包名追加进加载列表,每个被加载的包在@elementor/editor渲染之前运行一次init()完成注册。这一机制在 overview.md 中有完整描述。
从 core/editor/loader/editor-loader.php 的实现可以看到包加载的完整生命周期:
- Load—— 每个包经
wp_enqueue_script入队,依赖来自构建产物.asset.php; - Env——
elementor/editor/v2/scripts/env过滤器产出运行期配置,写入elementorEditorEnv全局; - Init extensions—— 依次调用
window.elementorV2.{packageName}?.init?.(); - Start app—— 调用
window.elementorV2.editor.start( domElement )启动 React 外壳。
值得注意的是,init()的执行顺序遵循**脚本依赖图(dependency graph)**而非 PHP 过滤器数组的书写顺序。另外,Editor_Loader::apply_editor_filter()揭示了过滤器链的实际走向:elementor/editor/{hook}→elementor/editor/v1/{hook}→elementor/editor/v2/{hook}。也就是说,本文使用的elementor/editor/v2/packages是三级链路的最后一环,既兼容旧扩展,也面向新包。
二、PHP 端注册包:两个过滤器
扩展的第一步是让 WordPress 后端"认识"你的包。示例文档给出两个过滤器,一个负责把包名加入加载列表,一个负责注入运行期环境配置:
add_filter( 'elementor/editor/v2/packages', function ( array $packages ) { return array_merge( $packages, [ 'editor-my-feature' ] ); } ); add_filter( 'elementor/editor/v2/scripts/env', function ( array $env ) { $env['@elementor/editor-my-feature'] = [ 'enabled' => true ]; return $env; } );要点拆解:
- 第一个过滤器接收当前已注册的包名数组(
$packages),用array_merge追加你的包名'editor-my-feature'。包名是字符串,最终会映射到window.elementorV2.editorMyFeature(camelCase 化)并触发其init()。 - 第二个过滤器以
@elementor/{包名}为键写入任意 JSON 可序列化的配置,前端包可通过@elementor/env等机制读取。示例中的enabled只是示范,实际可放入任何业务数据。
仓库内最直接的"标准答案"是 modules/site-navigation/module.php:
const PACKAGES = [ 'editor-site-navigation', ]; // 构造函数中: add_filter( 'elementor/editor/v2/packages', fn( $packages ) => $this->add_packages( $packages ) ); add_filter( 'elementor/editor/v2/scripts/env', function( $env ) { $env['@elementor/editor-site-navigation'] = [ 'is_pages_panel_active' => Plugin::$instance->experiments->is_feature_active( self::PAGES_PANEL_EXPERIMENT_NAME ), ]; return $env; } );可以看到生产代码的两种进阶做法:其一,用Plugin::$instance->experiments->is_feature_active()把实验开关(Experiment)状态注入 env,前端据此决定是否渲染面板;其二,包名收敛为模块常量PACKAGES,由array_merge( $packages, self::PACKAGES )统一合并,便于集中管理。文档 extending-editor.md 还展示了一种"按条件返回"的写法——功能未激活时直接返回原数组、不注册包,实现延迟/按需加载。
三、JS 端创建包:init()契约与入口导出
包的核心是src/init.ts中的init()函数。构建体系(webpack 构建尾部)会自动调用window.elementorV2.{camelCasePackage}?.init?.()——例如包名editor-my-feature对应window.elementorV2.editorMyFeature。init()是同步注册函数:只做注册,不负责渲染,渲染工作交给注入到各槽位的 React 组件完成。
init()契约在 overview.md 中定义为:
window.elementorV2.{packageName}?.init?.();示例文档给出的init.ts结构:
import { injectIntoPageIndication, toolsMenu } from '@elementor/editor-app-bar'; import { getMCPByDomain } from '@elementor/editor-mcp'; import { z } from '@elementor/schema'; import { MyIndicator } from './components/my-indicator'; import { useMyToggleProps } from './hooks/use-my-toggle-props'; export function init() { injectIntoPageIndication( { id: 'my-indicator', component: MyIndicator, } ); toolsMenu.registerToggleAction( { id: 'toggle-my-panel', priority: 20, useProps: useMyToggleProps, } ); const mcp = getMCPByDomain( 'my_feature', { instructions: 'Short hint for agents', docs: 'Full domain documentation', } ); mcp.addTool( { name: 'my_tool', description: 'Does something in the editor', schema: { elementId: z.string().describe( 'Target element id' ), }, handler: async ( { elementId } ) => `Handled ${ elementId }`, } ); }同时必须从src/index.ts再导出init,构建脚本才能识别包入口:
// src/index.ts export { init } from './init';仓库内最贴近此示例的真实实现是 packages/packages/core/editor-site-navigation/src/init.ts:它同样调用injectIntoPageIndication()注册顶部栏组件,通过toolsMenu.registerToggleAction()注册带priority: 6的开关动作,并在实验开关激活时用registerPanel( panel )注册滑入面板——id、component、priority、useProps等参数与示例一一对应,是阅读 API 用法的绝佳范本。
四、注入 API 全景:六大扩展区域
示例文档用一张表概括了常用注入 API,按"编辑器区域 → 注入 API → 所属包"组织:
| 区域 | API | 所属包 |
|---|---|---|
| Shell 外壳 | injectIntoTop、injectIntoLogic | @elementor/editor |
| 顶栏 App bar | injectIntoPageIndication、toolsMenu.registerToggleAction | @elementor/editor-app-bar |
| 样式选项卡 | injectIntoStyleTab | @elementor/editor-editing-panel |
| 元素面板 | injectTab | @elementor/editor-elements-panel |
| 样式仓库 | stylesRepository.register | @elementor/editor-styles-repository |
| v1 桥接 | registerDataHook、blockCommand、__privateListenTo | @elementor/editor-v1-adapters |
对照 extending-editor.md 的完整 Public API 表,还可补充几个同族入口:
- 编辑面板替换:
registerEditingPanelReplacement(@elementor/editor-editing-panel),按元素类型条件替换整个面板; - 站点设置选项卡:
injectSiteSettingsTab(@elementor/editor-site-settings); - 滑入面板:
registerPanel(@elementor/editor-panels); - Redux slice:
__registerSlice(@elementor/store); - MCP 领域:
getMCPByDomain(@elementor/editor-mcp)。
从 packages/packages/core/editor/src/index.ts 的导出可以看到 Shell 层的两个注入点injectIntoTop、injectIntoLogic与start()同源发布,这印证了"外壳槽位由@elementor/editor提供"的设计。需要做"旧事件桥接"的场景(React UI 里监听 v1 编辑器事件),则依赖@elementor/editor-v1-adapters的registerDataHook、blockCommand,以及listenTo( v1ReadyEvent(), fn )——注意带__private前缀的导出属于内部 API,不建议在生产代码中使用。
五、MCP 命名空间与工具注册
Editor V2 的包扩展不仅是 UI 注入,还包含编辑器内 MCP(Model Context Protocol)层:包可在init()中调用getMCPByDomain()暴露能力给 Angie 与 WebMCP。这与 PHP 端modules/mcp/的"能力(abilities)"是两套独立体系——本文示例文档特别标注了这一点:这里注册的是插件自有包内的 in-editor MCP 工具,不涉及 PHP MCP abilities。
两条硬性规则必须遵守:
- 命名空间格式:必须是
/^[a-z_]+$/,即仅小写字母加下划线。示例中的'my_feature'符合规范,my-feature之类的连字符命名不被接受; - Schema 类型:
addTool的schema字段是来自@elementor/schema的Zod 对象,而非纯 JSON Schema。运行时由 registry 将 Zod raw shape 转换为 JSON Schema 交给宿主。
getMCPByDomain的完整语义可在 registering-editor-tools.md 中找到:它返回/创建名为editor-{namespace}的领域服务器(domain server),可传入instructions(给 Agent 的简短提示)与docs(领域完整文档)。其中options.docs会自动注册elementor://{namespace}/server-docs资源,并合并进工具的requiredResources——这正是示例中docs: 'Full domain documentation'一行背后的机制。
addTool的字段语义(同样引自上述文档):
| 字段 | 用途 |
|---|---|
name | 暴露给宿主的工具名 |
description | 面向 Agent 的工具描述 |
schema | Zod raw shape,运行期转 JSON Schema |
outputSchema | 可选;启用后自动附加errors字段 |
isDestructive | 映射到destructiveHint,标记破坏性操作 |
requiredResources | { uri, description }[],会前置拼接到工具描述中 |
handler | 异步(args) => result;抛错时宿主侧收到isError: true |
六、验证清单:如何确认扩展生效
示例文档以三条可观测结果收尾,构成最小验证闭环:
- 包出现在编辑器网络请求中:打开浏览器开发者工具,过滤
js/packages/,应能看到你的包脚本被加载。注意 editor-loader.php 的命名规律是{assets_url}js/packages/{package}/{package}{min_suffix}.js,例如js/packages/editor-my-feature/editor-my-feature.min.js; - UI 渲染在指定槽位:注入的组件应出现在所选区域——顶栏指示器出现在页面指示区,切换按钮出现在工具菜单中(可观察
priority对排序的影响); - MCP 工具可见:当 Angie / WebMCP 实验启用后,
my_tool应出现在 Agent 可用工具列表,且elementId参数按 Zod schema 校验。
七、源码级进阶:从示例到生产实践的差异
将示例与仓库真实代码对照,还能提炼出三条生产级实践:
1. 用实验开关控制加载。site-navigation 模块在 env 中注入is_pages_panel_active供前端判断,同时仅当实验激活时才注册 REST 字段。示例中enabled: true的写法可升级为Plugin::$instance->experiments->is_feature_active( ... )驱动的真实开关。
2. 创建新包有完整流程。如果功能足够独立、需要新建包而非复用现有包,参考 packages/docs/creating-a-new-package.md:新建目录与package.json→ 编写源码 → 在 Demo 应用测试 → 在 Elementor 插件内联调 → 补单元测试 → 提 PR。加入插件的方式有两种:直接写死在core/editor/loader/editor-loader.php的包列表(始终加载),或走过滤器(条件加载、便于模块化封装)。
3. 基础库与扩展包分离。Editor_Loader::LIBS(见 editor-loader.php)维护了locations、store、schema、editor-v1-adapters等基础库,EXTENSIONS维护默认扩展;你的包通过elementor/editor/v2/packages追加后,与基础库一起入队、按依赖图排序初始化。
八、延伸阅读
- 编辑器包扩展完整指南——本示例的上游详版文档,含
init()契约、@elementor/locations位置表与更多代码示例; - 编辑器包总览——包分类、生命周期与加载器内部实现;
- 注册编辑器内 MCP 工具——
getMCPByDomain、addTool字段、适配器模式与命名空间规则的权威出处; - Site Navigation 模块真实实现——PHP 注册的最佳实践范本;
- Editor Site Navigation 包 init 实现——
init()中组合injectIntoPageIndication、toolsMenu.registerToggleAction与registerPanel的完整范例。
【免费下载链接】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),仅供参考