ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

lowcode-engine 编排模块设计深度解析:从 Schema 到节点模型、画布渲染与拖拽定位机制

2026/9/14 17:43:21 拓冰建站 浏览量
lowcode-engine 编排模块设计深度解析:从 Schema 到节点模型、画布渲染与拖拽定位机制 lowcode-engine 编排模块设计深度解析从 Schema 到节点模型、画布渲染与拖拽定位机制【免费下载链接】lowcode-engineAn enterprise-class low-code technology stack with scale-out design / 一套面向扩展设计的企业级低代码技术体系项目地址: https://gitcode.com/GitHub_Trending/lo/lowcode-engine本篇技术文章基于低代码引擎lowcode-engine官方设计文档系统讲解其编排模块designer从零设计到功能实现的完整思路为什么用仿 DOM 的节点模型替代直接操作 JSON、设计态与渲染态双层架构如何划分职责、设置面板与设置器如何驱动属性修改、拖拽引擎 Dragon 的感应区Sensor与定位机制如何工作以及 schema 管道、metadata 管道等扩展机制。读完你能掌握引擎模型分层的设计原理并能在插件开发中正确操作Node、Prop、ComponentMeta等核心模型。一、编排是什么本质又是什么所谓编排即将设计器中的所有物料进行布局设置、组件设置、交互设置JS 编写/逻辑编排后形成符合业务诉求的 schema 描述。编排的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据。在这个场景里协议通过 JSON 承载例如一个最简页面 schema{ componentName: Page, props: { layout: wide }, children: [ { componentName: Button, props: { size: large } } ] }真实场景中节点数可能有成百上千每个节点都具有新增、删除、修改、移动、插入子节点等操作同时还有若干约束直接操作 JSON 很不便利。于是引擎仿 DOM 设计了节点模型与属性模型用更具可编程性的方式来编排这是编排系统的基石。其次每次编排动作CRUD后都需要实时渲染出视图。广义的视图包括各种平台上的展现——浏览器、Rax、小程序、Flutter 等等——所以使用何种渲染器去渲染 JSON 结构应该可以由用户扩展引擎定义了一种机制去衔接设计态和渲染态。至此编排模块最基础的功能就完成了。接下来是完善细节、逐步丰满功能文档列出了这些演进方向编排面板的整体功能区划分设计节点属性设计节点删除、移动等操作设计容器节点设计节点拖拽功能、拖拽定位设计和实现节点在画布上的辅助功能比如 hover、选中、选中时的操作项、resize、拖拽占位符等设计态和渲染态的坐标系转换滚动监听等快捷键机制历史功能撤销和重做结构化的插件扩展机制原地编辑功能。模块很多但核心原则只有一条这些功能的目的都是辅助用户在画布上有更好的编排体验和扩展能力而逐个增加设计的。二、模型设计schema 的分层职责编排实际上操作 schema但在代码运行过程中引擎将 schema 分成很多层每一层有各自的职责负责的功能明确清晰。通过将 schema 和常用操作结合起来低代码引擎的模型被划分为项目模型、文档模型、节点模型、属性模型以及描述组件约束的 ComponentMeta。项目模型Project项目模型提供项目管理能力。通常一个引擎启动会默认创建一个Project实例有且只有一个。项目模型实例下可以持有多个文档模型实例而当前处于设计器设计状态的文档模型会添加 active 标识被称为currentDocument可以通过project.currentDocument获得。一个Project包含若干个DocumentModel实例即项目模型和文档模型是1 对 n的关系。文档模型DocumentModel文档模型提供文档管理的能力每一个页面即一个文档流对应一个文档模型。文档模型包含了一组Node组成的一棵树类似于 DOM。我们可以通过文档模型来操作Node树达到管理文档模型的目的。每一个文档模型对应多个Node但根Node只有一个即rootNode。文档模型可以通过Node树、通过doc.schema导出文档的 schema并使用其进行渲染。节点模型Node先看一个Node在 schema 中的对应示例{ componentName: Text, id: node_k1ow3cbf, props: { showTitle: false, behavior: NORMAL, content: { use: zh_CN, en_US: Title, zh_CN: 个人信息, type: i18n, }, fieldId: text_k1ow3h1j, maxLine: 0, }, condition: true, }上面的示例是一个Text节点而Node节点模型就是负责这一层级的 schema 管理其功能聚焦于单层级的 schema 相关操作。文档给出了节点模型的核心方法面declare class NodeSchema extends NodeSchema NodeSchema { // Props props: Props; get propsData(): PropsMap | PropsList | null; getProp(path: string, stash?: boolean): Prop | null; getPropValue(path: string): any; setPropValue(path: string, value: any): void; clearPropValue(path: string): void; mergeProps(props: PropsMap): void; setProps(props?: PropsMap | PropsList | Props | null): void; // Node get parent(): ParentalNode | null; get children(): NodeChildren | null; get nextSibling(): Node | null; get prevSibling(): Node | null; remove(useMutator?: boolean, purge?: boolean): void; select(): void; hover(flag?: boolean): void; replaceChild(node: Node, data: any): Node; mergeChildren(remover: () any, adder: (children: Node[]) NodeData[] | null, sorter: () any): void; removeChild(node: Node): void; insert(node: Node, ref?: Node, useMutator?: boolean): void; insertBefore(node: any, ref?: Node, useMutator?: boolean): void; insertAfter(node: any, ref?: Node, useMutator?: boolean): void; // Schema get schema(): Schema; set schema(data: Schema); export(stage?: TransformStage): Schema; replaceWith(schema: Schema, migrate?: boolean): any; }从源码实现看node.ts这些方法确实按上述职责分组落地Props 管理getProp委托给this.props.query(path, createIfNone)完成按路径的属性查询getProp 实现setPropValue通过getProp(path, true)!.setValue(value)写入setPropValue 实现mergeProps与setProps分别对应合并与替换两种多属性写入语义mergeProps/setProps 实现。Node 树管理index是一个computed计算属性通过父节点的children.indexOf(this)得出当前节点在父容器中的位置nextSibling/prevSibling则基于该索引访问兄弟节点nextSibling/prevSibling 实现insertAfter等插入操作同样在此文件中实现insertAfter 实现。Schema 管理get schema的底层实现是this.export(IPublicEnumTransformStage.Save)即按保存阶段导出当前层级的 schemaschema getter 实现。Node节点模型核心功能点归纳为三个Props管理通过Props实例管理所有的Prop包括新增、设置、删除等Prop相关操作Node管理管理Node树的关系修改当前Node节点或者Node子节点等Schema管理可以通过Node获取当前层级的 schema 描述协议内容并且也可以修改它。通过Node这一层级把Props、Node树和Schema的管理粒度控制到最低扩展性也就更强。属性模型Prop一个Props对应多个Prop每一个Prop对应 schema 的props下的一个字段。Props管理的是Node节点模型中props字段下的内容而Prop管理的是props下的每一个 key。以下面的示例为例一个Props管理至少 6 个Prop其中一个Prop管理的是showTitle的值{ props: { showTitle: false, behavior: NORMAL, content: { use: zh_CN, en_US: Title, zh_CN: 个人信息, type: i18n, }, fieldId: text_k1ow3h1j, maxLine: 0, }, }组件描述模型ComponentMeta编排已经等价于直接操作节点 属性了而一个节点和一组对应的属性相当于一个真实的组件。真实的组件一定是有约束的组件名、组件类型、支持哪些属性以及属性类型、组件能否拖动、支持哪些扩展操作、是否是容器型组件、A 组件中能否放入 B 组件等等。于是引擎设计了一份协议专门负责组件描述——《中后台搭建组件描述协议》编排模块中也有负责解析和使用符合描述协议规范的模块。每一个组件对应一个ComponentMeta实例其属性和方法就是描述协议中的所有字段。所有ComponentMeta都由设计器的designer模块创建和管理其他模块通过designer获取指定的ComponentMeta实例尤其是每个Node实例上都会挂载对应的ComponentMeta实例源码中Node接口确实声明了get componentMeta(): IComponentMeta见 Node 接口定义。组件描述模型是后续编排辅助的基础包括设置面板、拖拽定位机制等。项目、文档、节点和属性模型的关系整体来看一个Project包含若干个DocumentModel实例每个DocumentModel包含一组Node构成一棵树类似 DOM 树每个Node通过Props实例管理所有Prop。节点 属性模型是引擎基石几乎贯穿所有模块。节点 属性模型等价于 JSON 数据结构而编排的本质是产出 JSON 数据结构——现在可以重新表述为编排的本质是操作节点 属性模型。// 一段编排的示例代码 rootNode.insertAfter({ componentName: Button, props: { size: medium } }); rootNode.insertAfter({ componentName: Button, props: { size: medium } }); rootNode.children.get(1).getProp(size).setValue(large); rootNode.children.get(2).remove(); rootNode.export(); // 产出 schema这几行代码即完整体现了模型的价值两次插入、一次属性修改、一次删除、一次导出全程无需触碰原始 JSON。测试用例中也有同类操作的实际验证如 node.test.ts 与 document-model.test.ts 中对rootNode.children.get(n)、setProps等 API 的使用。三、画布渲染设计态与渲染态的双层架构画布渲染使用了设计态与渲染态的双层架构。设计器和渲染器处在不同的 Frame 下渲染器以单独的iframe嵌入。这样做的好处一是给渲染器一个更纯净的运行环境更贴近生产环境二是扩展性考虑让用户基于接口约束自定义自己的渲染器。xxx-rendererxxx-renderer是一个纯渲染器给定输入 schema、依赖组件和配置参数之后完成渲染。官方 React 实现见 react-renderer。xxx-simulator-rendererxxx-simulator-renderer通过与 host 通信来和设计器打交道从DocumentModel获取 schema 和组件将其传入xxx-renderer完成渲染。官方 React 实现位于 react-simulator-renderer。此外它还提供了一些必要的接口帮助设计器完成交互。比如点击渲染画布任意一个位置需要能计算出点击的组件实例继而找到设计器对应的Node实例以及组件实例的位置/尺寸信息让设计器完成辅助 UI 的绘制如节点选中。react-simulator-renderer 的点击定位实现以官方react-simulator-renderer为例点击一个 DOM 节点后编排模块的处理链路是初始化打标renderer 渲染时给每一个元素添加 ref通过 ref 机制在组件创建时将其存储起来并给实例添加Symbol(_LCNodeId)属性。源码印证了这一标记renderer.ts 中定义了const SYMBOL_VNID Symbol(_LCNodeId)。点击后查 fiber 树根据__reactInternalInstance$查找相应的 fiberNode通过递归查找到对应的 React 组件实例找到一个挂载着Symbol(_LCNodeId)的实例——即上面初始化时添加的属性对应实现getClosestNodeInstance见 getClosestNodeInstance 方法。反查 Node通过Symbol(_LCNodeId)属性获取 Node 的 id从而找到 Node 实例。取几何信息通过getBoundingClientRect获取 Node 渲染出来的 DOM 信息包括x、y、width、height等对应getClientRects方法见 getClientRects 方法其底层工具实现在 get-client-rects.ts。绘制辅助 UI通过 DOM 信息将 focus 节点所需的标志渲染到对应位置。hover、拖拽占位符、resize handler 等辅助 UI 都是类似逻辑。通信机制host 中间层既然设计器和渲染器处于两个 Frame它们之间的事件通信、方法调用是通过各自的代理对象进行的不允许其他方式以避免代码耦合。由于 renderer 层不负责与设计器相关的交互所以增加了一层host作为通信的中间层host可以访问设计器的所有模块并提供相关方法供 simulator-renderer 层调用例如 schema 的获取、组件获取等。simulator-renderer 通过调用 host 的方法将 schema、components 等参数传给 renderer 完成渲染。xxx-simulator-renderer为了完成双向交互也需要提供方法供 host 层调用。当设计器和用户有交互例如节点选中时由 host 反向调用这些方法。需要提供的接口有getClientRectsgetClosestNodeInstancefindDOMNodesgetComponentsetNativeSelectionsetDraggingStatesetCopyStateclearState这样host 和 simulator-renderer 之间便通过相关方法实现了双向通信能在隔离设计器的基础上完成设计器到画布和画布到设计器的通信流程。四、编排辅助的核心设置面板与设置器当在渲染画布上点击一个 DOM 节点我们可以通过xxx-simulator-renderer获取Node节点而Node上挂载了ComponentMeta实例。通过ComponentMeta获取当前组件的描述模型即可获得组件即当前 Node支持的所有属性配置。设置面板设置面板对于配置项的呈现结构是通过ComponentMeta.configure来确定的。文档给出了一个典型的 configure 片段{ component: { isContainer: true }, props: { isExtends: true, override: [ { name: count, title: { label: 展示的数字, tip: count|大于 overflowCount 时显示为 ${overflowCount}为 0 时默认隐藏, docUrl: https://fusion.alibaba-inc.com/pc/component/basic/badge }, setter: { componentName: MixedSetter, props: { setters: [ StringSetter, ExpressionSetter ] } } } ] } }component.isContainer描述这个组件是否是容器组件props下的属性就是设置面板中展示的属性包含属性的名称、使用的设置器、配置之后影响的是哪个属性等。而这只是描述。编排模块中真正管理设置面板的实现模块是SettingTopEntry它包含 n 个SettingField每一个SettingField对应一个设置器即SettingTopEntry负责管理多个SettingField。源码中该模块位于 setting 目录其中setting-top-entry.ts与setting-field.ts分别对应这两个角色。设置器选中节点可供配置的属性都有相应的设置器配置比如文本、数字、颜色、JSON、Choice、I18N、表达式等等或者混合多种。设置器本质上是一个 React 组件但设置面板在渲染时会传入当前配置项对应的SettingField实例——SettingField本质上就是包裹了Prop实例。设置器内部的行为以及 UI 变化都由设置器自己把控但当属性值发生变化时必须通过SettingField下的Prop来修改值因为修改Prop实例就相当于修改了 schema一方面这样的设置之后保存的 schema 才是正确的另一方面只有 schema 变化了才能触发渲染画布重新渲染。这是设置器不直接持有状态、一切经由模型的设计约束。拖拽引擎 拖拽定位机制拖拽引擎Dragon核心完成的工作是将被拖拽对象拖拽到目标位置。涉及几个概念被拖拽对象 -DragObject拖拽到的目标位置 -DropLocation拖拽感应区 -IPublicModelSensor定位事件 -LocateEventSensor引擎初始化的时候监听document和 iframecontentDocument的mouse、keyboard、drag事件来感知拖拽的发生。这些监听的区域被称为拖拽感应区即Sensor。Sensor会有多个因为感应器有多个——默认设置器和设置面板是没有Sensor的但它们可以注册Sensor来增加感应区域例如大纲树就注册了自己的Sensor。Sensor有两个关键职责用于事件对象转换比如坐标系换算根据拖拽过程中提供的位置信息结合每一层Node组件描述信息中是否能作为容器等限制条件进行进一步定位最后计算出精准信息来进行视图渲染。拖拽流程引擎初始化的时候初始化多个Sensor当拖拽开始时开启mousemove、mouseleave、mouseover等事件的监听拖拽过程中根据mousemove的MouseEvent对象封装出LocateEvent对象继而交给相应sensor做进一步定位处理拖拽结束时根据拖拽的结果进行 schema 变更和视图渲染最后关闭拖拽开始时的事件监听。从源码结构看dragon.ts 中的Dragon类class Dragon维护着private sensors: IPublicModelSensor[]与响应式的_activeSensor在boost阶段通过chooseSensor(locateEvent)从已注册传感器与 masterSensors 中挑选当前命中的感应区createLocateEvent则负责把原生MouseEvent封装为LocateEvent——与文档描述的监听 → 封装 LocateEvent → sensor 定位 → 结束提交流程一一对应。拖拽方式根据拖拽对象不同拖拽分为几种方式画布内拖拽此时 sensor 是simulatorHost拖拽完成后根据拖拽的位置完成节点的精确插入从组件面板拖拽到画布sensor 还是simulatorHost因为拖拽结束的目标还是画布大纲树面板拖拽到画布中有两个 sensor一个是大纲树当拖拽到画布区域时画布区域内的simulatorHost开始接管画布拖拽到大纲树中从画布中开始拖拽时最新生效的是simulatorHost当离开画布到大纲树时大纲树 sensor 开始接管生效。当拖拽到大纲树的某一个节点下时大纲树会将大纲树中的信息转化为 schema然后渲染到画布中。五、其他关键机制引擎的编排能力远不止上述核心功能。在整个引擎的迭代与设计过程中还有很多细节使其更好用、更容易扩展schema 处理的管道机制通过PropsReducer的管道机制用户可以定制自己需要的逻辑来修改 Schema。在源码中该机制挂载于设计器核心designer.ts 中实现了 reducer 相关逻辑插件可以借此在属性流转的各个环节注入自定义处理。组件 metadata 处理的管道机制组件的描述信息都收拢在各自的ComponentMeta实例内涉及到的消费方几乎遍及整个编排过程包括但不限于组件拖拽、拖拽辅助 UI、设置区、原地编辑、大纲树等等。在用户需要自定义的场景开放ComponentMeta的修改能力至关重要因此引擎设计了 metadata 初始化/修改的管道机制。hotkey builtin-hotkey快捷键的实现以及引擎内核默认绑定的快捷键行为。drag resize 引擎对于布局等类型的组件支持拖拽改变大小。resize 拖拽引擎根据组件ComponentMeta声明来开启拖拽后触发组件的钩子函数onResizeStart/onResize/onResizeEnd完成 resize 过程。OffsetObserver设计态的辅助 UI 需要根据渲染态的视图变化而变化比如渲染容器滚动了此时通过OffsetObserver做动态监听。实现见 offset-observer.ts 中的OffsetObserver类它服务于设计态与渲染态之间的坐标系同步。插件机制引擎希望保持内核足够小但拥有足够强的扩展能力所有扩展功能都通过插件机制来承载。六、小结lowcode-engine 的编排模块设计可以概括为一条主线把产出搭建协议 JSON这一本质落到仿 DOM 的 Project → DocumentModel → Node → Props/Prop 分层模型上再由 host simulator-renderer 的双层架构把设计态变更实时渲染到隔离的 iframe 画布最后以ComponentMeta描述协议为枢纽驱动设置面板、拖拽定位、原地编辑等一系列辅助能力并通过 schema 管道、metadata 管道与插件机制保持内核精简而扩展开放。对开发者而言理解这套模型分层与通信边界后无论是编写插件操作Node/Prop还是自定义 setter 与 renderer都有了清晰的着力点。【免费下载链接】lowcode-engineAn enterprise-class low-code technology stack with scale-out design / 一套面向扩展设计的企业级低代码技术体系项目地址: https://gitcode.com/GitHub_Trending/lo/lowcode-engine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考