低代码表单设计器项目源码解析项目地址lowcode-form-builder效果展示空模板自带模板预览效果:高亮效果一、 目录结构说明项目按功能模块进行了目录拆分主要结构如下├── index.html // 页面布局及组件的 template 模板库 ├── app.css // 全局 CSS 样式 └── js/ ├── main.js // 入口文件负责事件总线绑定 ├── core/ │ ├── state.js // 全局状态管理及历史记录撤销/重做 │ └── schema.js // 数据导出逻辑 ├── components/ │ ├── registry.js // 组件物料的默认属性配置 │ └── builder.js // 读取模板生成真实 DOM 节点的工厂函数 ├── ui/ │ ├── canvas.js // 画布区域逻辑初始化、拖拽绑定、平移缩放 │ ├── properties.js // 属性面板逻辑监听输入并同步 DOM │ └── preview.js // 代码与 JSON 预览模块 └── utils/ └── helpers.js // 辅助工具防抖、唯一 ID 生成等二、 核心数据模型画布上的所有操作均会同步更新到统一的 JSON 数据模型中核心状态定义在state.js。1. 数据结构数据按页面划分pages数组存放各页面的基本属性及其包含的表单元素elements。// js/core/state.jsexportconststate{pages:[{id:page_1,title:第一页,description:,elements:[]// 存放画布中的组件数据}],selectedElement:null,// 当前在画布中被选中的 DOM 节点// 历史记录数组undoStack:[],redoStack:[],// 画布空间矩阵状态canvasState:{scale:1,x:0,y:0},isPanning:false};三、 画布初始化与基本事件画布的交互由js/ui/canvas.js处理。主要包含多页面的初始化、选中元素的高亮控制。1. 页面初始化读取state.pages数据并为每一个 page 生成对应的容器节点。// js/ui/canvas.jsexportfunctioninitPages(){DOM.canvasInner.innerHTML;// 清空画布state.pages.forEach((page,index){// 通过克隆 template idtpl-page-frame 创建页面框架consttemplatedocument.getElementById(tpl-page-frame);constframetemplate.content.cloneNode(true).firstElementChild;frame.idframe_page.id;// 绑定该页面的基础数据frame.dataset.titlepage.title||第${index1}页;frame.dataset.descriptionpage.description||;DOM.canvasInner.appendChild(frame);// 获取该页面内部的组件接收区并初始化拖拽能力constdropzoneframe.querySelector(.canvas-dropzone);dropzone.idpage.id;initDropzone(dropzone);});}2. 元素选中控制点击画布内的组件时需要为其增加高亮样式并通知属性面板更新数据。// js/ui/canvas.jsexportfunctionselectElement(element){if(!element)return;// 1. 移除全局其他元素的选中状态clearSelection();// 2. 更新选中状态记录state.selectedElementelement;// 3. 区分大画布和普通表单组件添加对应的高亮 CSS 类名if(element.classList.contains(mobile-frame)){element.classList.add(ring-2,ring-[#1677ff],ring-offset-4);}else{element.classList.add(is-selected);}// 4. 调用属性面板的更新方法updatePropPanel(element);}exportfunctionclearSelection(){// 移除各类高亮类名document.querySelectorAll(.is-selected).forEach(elel.classList.remove(is-selected));document.querySelectorAll(.mobile-frame).forEach(f{f.classList.remove(ring-2,ring-[#1677ff],ring-offset-4);});state.selectedElementnull;// 通知属性面板展示空状态updatePropPanel(null);}## 四、 拖拽引擎实现 表单画布的拖拽依赖Sortable.js库。系统将拖拽行为分为“物料区克隆”与“画布区排序嵌套”两部分。 ###1.画布接收区逻辑 当组件从物料区拖入画布时会触发onAdd生命周期。在此过程中必须将基础的拖拽DOM节点替换为真实的表单组件。javascript// js/ui/canvas.jsexportfunctioninitDropzone(dropzoneEl){Sortable.create(dropzoneEl,{group:{name:shared,pull:true,put:true},animation:150,fallbackOnBody:true,// 防止拖拽幽灵被遮挡onAdd:function(evt){constitemevt.item;consttypeitem.dataset.type;// 1. 获取对应的真实 DOM 节点constnewElcreateComponentDOM(type);// 2. 暂时关闭 CSS transition 以防止闪烁变形item.style.transitionnone;// 3. 替换 DOM 结构与 IDitem.innerHTMLnewEl.innerHTML;item.classNamenewEl.className;item.idnewEl.id;// 4. 强制引发浏览器重排 (Reflow)voiditem.offsetHeight;// 5. 恢复动画属性item.style.transition;// 6. 如果是栅格布局容器递归绑定拖拽能力if(typegrid){item.querySelectorAll(.nested-dropzone).forEach(zone{initDropzone(zone);});}// 7. 保存到历史快照saveHistoryState();}});}五、 无限嵌套布局实现表单中常需要双列或多列栅格布局Grid。在initDropzone函数中通过检测拖入的组件类型是否为grid来决定是否递归执行初始化逻辑。// 递归判断并初始化嵌套容器if(typegrid){constzonesnewEl.querySelectorAll(.nested-dropzone);zones.forEach(zoneinitDropzone(zone));}这种递归结构允许栅格内部嵌套新的栅格通过同样的shared分组实现了无限层级的拖拽。六、 动态组件模板克隆所有的具体组件 DOM 骨架预先定义在index.html的template标签中。生成组件时直接深拷贝该节点避免在 JS 中拼接 HTML 字符串。// js/components/builder.jsexportfunctioncreateComponentDOM(type){consttemplatedocument.getElementById(tpl-${type});if(!template)returnnull;// cloneNode(true) 实现深拷贝constrootDivtemplate.content.cloneNode(true).firstElementChild;rootDiv.dataset.typetype;rootDiv.idcmp_${Date.now()};// 实际采用全局自增唯一 ID// 读取默认配置 (如 label、placeholder) 注入 DOMconstdefaultDatacomponentDefaults[type];Object.keys(defaultData).forEach(key{rootDiv.dataset[key]defaultData[key];});returnrootDiv;}七、 右侧面板数据联动当用户在画布中选中某个组件后右侧属性面板展示对应数据。用户在输入框中修改配置时需要实时同步回画布中的 DOM 节点。1. 局部事件代理更新 DOM为了提升性能采用局部事件代理仅替换对应文本节点保留其他 HTML 结构如必填标志的红色星号。// js/ui/properties.jsDOM.inputLabel.addEventListener(input,e{if(!state.selectedElement)return;// 1. 写入内存数据state.selectedElement.dataset.labele.target.value;// 2. 定点查询画布内被选中元素的对应节点constlabelNodestate.selectedElement.querySelector(.component-label);if(labelNode){conststarlabelNode.querySelector(.req-star);// 更新文本内容labelNode.textContente.target.value ;// 将星号节点重新挂载if(star)labelNode.appendChild(star);}});2. 虚拟焦点高亮机制当属性面板输入框获得焦点时左侧画布内相应的精确元素会高亮以便用户明确当前编辑的目标。// ui/properties.jsconstbindVirtualFocus(propInput,selector){propInput.addEventListener(focus,(){if(!state.selectedElement)return;consttargetstate.selectedElement.querySelector(selector);if(target)target.classList.add(virtual-focus);// 添加高亮类名});propInput.addEventListener(blur,(){if(!state.selectedElement)return;consttargetstate.selectedElement.querySelector(selector);if(target)target.classList.remove(virtual-focus);});};// 实际绑定逻辑bindVirtualFocus(DOM.inputLabel,.label-text);bindVirtualFocus(DOM.inputHelp,.field-help);八、 画布的平移与缩放控制作为一个无边界的设计器画布需要支持平移Pan与缩放Zoom。在原生 JS 中这通过修改外层容器的 CSStransform矩阵来实现。为了避免频繁重绘导致的卡顿使用了requestAnimationFrame进行节流。// ui/canvas.jsdocument.addEventListener(mousemove,e{if(state.isPanning){e.preventDefault();// 计算鼠标当前位置与起始位置的差值state.canvasState.xstate.panStartCanvasX(e.clientX-state.panStartX);state.canvasState.ystate.panStartCanvasY(e.clientY-state.panStartY);// 保证样式更新在浏览器的最佳渲染帧执行requestAnimationFrame((){DOM.canvasInner.style.transformtranslate(${state.canvasState.x}px,${state.canvasState.y}px) scale(${state.canvasState.scale});});}});滚轮缩放同理通过监听wheel事件增减state.canvasState.scale的值并限制在0.25到2.0的合理范围内。九、 组件物料注册表 (Registry)在添加诸如“国家选择”、“单选框”等复杂组件时它们可能带有预设的选项数组。系统通过统一的物料注册表进行管理实现了代码配置的分离。// components/registry.jsexportconstcomponentDefaults{input:{label:单行文本,placeholder:请输入内容,help:,defaultValue:},radio:{label:单选框组,help:,defaultValue:option_1,layout:inline,options:[{label:选项一,value:option_1},{label:选项二,value:option_2}]}// ... 可以通过追加对象无限扩展新物料};这个注册表不仅为createComponentDOM提供了挂载依据也被导出逻辑读取。十、 表单代码反编译 (Code Generation)低代码引擎最终需要输出代码供研发使用。本项目实现了一个简易的代码生成器通过递归遍历state.pages数据模型生成标准的 UI 代码片段。// ui/preview.jsfunctionbuildReactComponentCode(schema){letcodeimport React, { useState } from react;\n\n;codeexport default function DynamicForm() {\n;codeconst [formData, setFormData] useState({});\n\n;codeconst handleSubmit (e) { e.preventDefault(); console.log(formData); };\n\n;codereturn (\n form onSubmit{handleSubmit} classNameform-container\n;// 遍历每一个画布 / 页面schema.pages.forEach(page{codediv classNameform-page-card id${page.id}\n;codeh2${page.title}/h2\n;// 遍历页面内的所有组件page.elements.forEach(elementData{codegenerateJSXForElement(elementData);});code/div\n;});codebutton typesubmitSubmit/button\n;code/form\n );\n}\n;returncode;}// 根据 type 拼接具体的输入框 JSXfunctiongenerateJSXForElement(elData){if(elData.typeinput){returndiv classNameform-item label${elData.label}/label input typetext placeholder${elData.placeholder} onChange{(e) setFormData({...formData,${elData.key}: e.target.value})} / /div\n;}// ... 补充其他类型return;}通过这一层转换逻辑复杂的树状 JSON 即可输出为便于开发人员直接复制使用的前端源码至此完成了从“可视化拖拽”到“最终代码生产”的闭环。十一、 项目持续演进与未来路线在最初实现了基础的拖拽搭建与代码导出后我们从未停止对核心引擎的打磨。针对工业级低代码平台的标准我们已经或正在推进以下演进方向1. 状态管理与数据打通 (State Schema Sync) -【已实现】我们不仅修复了由于跨模板切换引发的“组件 ID 计数器泄露”漏洞通过在loadSchema中自适应扫描底层节点并重置最高水位线更实现了画布 DOM 树与底层 JSON Schema 的完美双向数据打通。任何在右侧面板的修改、画布的拖拽都会实时反馈至底层模型使预览模块可以直接对接真实 Schema 数据。未来的演进则考虑在“撤销/重做”栈中引入类似 Immutable 数据结构或简易 Diff 算法以降低内存开销。2. 组件联动与逻辑编排 (Logic Flow)目前的表单组件之间是彼此独立的。下一步将引入规则引擎Rule Engine支持组件间的值联动Value Linkage与显隐控制。例如当“国家选择”选中“中国”时“省市联动组件”才显示。这需要在schema中定义dependencies或expressions字段并在核心引擎层增加表达式解析模块。3. API 持久化与动态数据源接入 (Mock API Remote Data) -【已实现与演进中】我们已经抽离出了api.js层利用 Promise 和 localStorage 构建了一套完整的伪后端Mock API服务完美打通了表单的“发布”、“加载初始模板”等流程保证了页面刷新不丢数据。下一步演进是针对组件本身如下拉框、单选框支持绑定远程 API 获取选项数据允许配置请求 URL 和 Response 解析路径在画布预览时动态渲染异步数据。4. 插件化自定义物料 (Custom Plugin System)目前的所有组件在index.html的template中写死。后续计划开发插件注入系统允许开发者遵循特定规范编写独立的.js或.html组件包通过全局方法window.LowcodeEngine.registerComponent()在运行时动态将自定义业务组件注入到左侧物料区中。
低代码平台表单设计器项目源码解析
低代码表单设计器项目源码解析项目地址lowcode-form-builder效果展示空模板自带模板预览效果:高亮效果一、 目录结构说明项目按功能模块进行了目录拆分主要结构如下├── index.html // 页面布局及组件的 template 模板库 ├── app.css // 全局 CSS 样式 └── js/ ├── main.js // 入口文件负责事件总线绑定 ├── core/ │ ├── state.js // 全局状态管理及历史记录撤销/重做 │ └── schema.js // 数据导出逻辑 ├── components/ │ ├── registry.js // 组件物料的默认属性配置 │ └── builder.js // 读取模板生成真实 DOM 节点的工厂函数 ├── ui/ │ ├── canvas.js // 画布区域逻辑初始化、拖拽绑定、平移缩放 │ ├── properties.js // 属性面板逻辑监听输入并同步 DOM │ └── preview.js // 代码与 JSON 预览模块 └── utils/ └── helpers.js // 辅助工具防抖、唯一 ID 生成等二、 核心数据模型画布上的所有操作均会同步更新到统一的 JSON 数据模型中核心状态定义在state.js。1. 数据结构数据按页面划分pages数组存放各页面的基本属性及其包含的表单元素elements。// js/core/state.jsexportconststate{pages:[{id:page_1,title:第一页,description:,elements:[]// 存放画布中的组件数据}],selectedElement:null,// 当前在画布中被选中的 DOM 节点// 历史记录数组undoStack:[],redoStack:[],// 画布空间矩阵状态canvasState:{scale:1,x:0,y:0},isPanning:false};三、 画布初始化与基本事件画布的交互由js/ui/canvas.js处理。主要包含多页面的初始化、选中元素的高亮控制。1. 页面初始化读取state.pages数据并为每一个 page 生成对应的容器节点。// js/ui/canvas.jsexportfunctioninitPages(){DOM.canvasInner.innerHTML;// 清空画布state.pages.forEach((page,index){// 通过克隆 template idtpl-page-frame 创建页面框架consttemplatedocument.getElementById(tpl-page-frame);constframetemplate.content.cloneNode(true).firstElementChild;frame.idframe_page.id;// 绑定该页面的基础数据frame.dataset.titlepage.title||第${index1}页;frame.dataset.descriptionpage.description||;DOM.canvasInner.appendChild(frame);// 获取该页面内部的组件接收区并初始化拖拽能力constdropzoneframe.querySelector(.canvas-dropzone);dropzone.idpage.id;initDropzone(dropzone);});}2. 元素选中控制点击画布内的组件时需要为其增加高亮样式并通知属性面板更新数据。// js/ui/canvas.jsexportfunctionselectElement(element){if(!element)return;// 1. 移除全局其他元素的选中状态clearSelection();// 2. 更新选中状态记录state.selectedElementelement;// 3. 区分大画布和普通表单组件添加对应的高亮 CSS 类名if(element.classList.contains(mobile-frame)){element.classList.add(ring-2,ring-[#1677ff],ring-offset-4);}else{element.classList.add(is-selected);}// 4. 调用属性面板的更新方法updatePropPanel(element);}exportfunctionclearSelection(){// 移除各类高亮类名document.querySelectorAll(.is-selected).forEach(elel.classList.remove(is-selected));document.querySelectorAll(.mobile-frame).forEach(f{f.classList.remove(ring-2,ring-[#1677ff],ring-offset-4);});state.selectedElementnull;// 通知属性面板展示空状态updatePropPanel(null);}## 四、 拖拽引擎实现 表单画布的拖拽依赖Sortable.js库。系统将拖拽行为分为“物料区克隆”与“画布区排序嵌套”两部分。 ###1.画布接收区逻辑 当组件从物料区拖入画布时会触发onAdd生命周期。在此过程中必须将基础的拖拽DOM节点替换为真实的表单组件。javascript// js/ui/canvas.jsexportfunctioninitDropzone(dropzoneEl){Sortable.create(dropzoneEl,{group:{name:shared,pull:true,put:true},animation:150,fallbackOnBody:true,// 防止拖拽幽灵被遮挡onAdd:function(evt){constitemevt.item;consttypeitem.dataset.type;// 1. 获取对应的真实 DOM 节点constnewElcreateComponentDOM(type);// 2. 暂时关闭 CSS transition 以防止闪烁变形item.style.transitionnone;// 3. 替换 DOM 结构与 IDitem.innerHTMLnewEl.innerHTML;item.classNamenewEl.className;item.idnewEl.id;// 4. 强制引发浏览器重排 (Reflow)voiditem.offsetHeight;// 5. 恢复动画属性item.style.transition;// 6. 如果是栅格布局容器递归绑定拖拽能力if(typegrid){item.querySelectorAll(.nested-dropzone).forEach(zone{initDropzone(zone);});}// 7. 保存到历史快照saveHistoryState();}});}五、 无限嵌套布局实现表单中常需要双列或多列栅格布局Grid。在initDropzone函数中通过检测拖入的组件类型是否为grid来决定是否递归执行初始化逻辑。// 递归判断并初始化嵌套容器if(typegrid){constzonesnewEl.querySelectorAll(.nested-dropzone);zones.forEach(zoneinitDropzone(zone));}这种递归结构允许栅格内部嵌套新的栅格通过同样的shared分组实现了无限层级的拖拽。六、 动态组件模板克隆所有的具体组件 DOM 骨架预先定义在index.html的template标签中。生成组件时直接深拷贝该节点避免在 JS 中拼接 HTML 字符串。// js/components/builder.jsexportfunctioncreateComponentDOM(type){consttemplatedocument.getElementById(tpl-${type});if(!template)returnnull;// cloneNode(true) 实现深拷贝constrootDivtemplate.content.cloneNode(true).firstElementChild;rootDiv.dataset.typetype;rootDiv.idcmp_${Date.now()};// 实际采用全局自增唯一 ID// 读取默认配置 (如 label、placeholder) 注入 DOMconstdefaultDatacomponentDefaults[type];Object.keys(defaultData).forEach(key{rootDiv.dataset[key]defaultData[key];});returnrootDiv;}七、 右侧面板数据联动当用户在画布中选中某个组件后右侧属性面板展示对应数据。用户在输入框中修改配置时需要实时同步回画布中的 DOM 节点。1. 局部事件代理更新 DOM为了提升性能采用局部事件代理仅替换对应文本节点保留其他 HTML 结构如必填标志的红色星号。// js/ui/properties.jsDOM.inputLabel.addEventListener(input,e{if(!state.selectedElement)return;// 1. 写入内存数据state.selectedElement.dataset.labele.target.value;// 2. 定点查询画布内被选中元素的对应节点constlabelNodestate.selectedElement.querySelector(.component-label);if(labelNode){conststarlabelNode.querySelector(.req-star);// 更新文本内容labelNode.textContente.target.value ;// 将星号节点重新挂载if(star)labelNode.appendChild(star);}});2. 虚拟焦点高亮机制当属性面板输入框获得焦点时左侧画布内相应的精确元素会高亮以便用户明确当前编辑的目标。// ui/properties.jsconstbindVirtualFocus(propInput,selector){propInput.addEventListener(focus,(){if(!state.selectedElement)return;consttargetstate.selectedElement.querySelector(selector);if(target)target.classList.add(virtual-focus);// 添加高亮类名});propInput.addEventListener(blur,(){if(!state.selectedElement)return;consttargetstate.selectedElement.querySelector(selector);if(target)target.classList.remove(virtual-focus);});};// 实际绑定逻辑bindVirtualFocus(DOM.inputLabel,.label-text);bindVirtualFocus(DOM.inputHelp,.field-help);八、 画布的平移与缩放控制作为一个无边界的设计器画布需要支持平移Pan与缩放Zoom。在原生 JS 中这通过修改外层容器的 CSStransform矩阵来实现。为了避免频繁重绘导致的卡顿使用了requestAnimationFrame进行节流。// ui/canvas.jsdocument.addEventListener(mousemove,e{if(state.isPanning){e.preventDefault();// 计算鼠标当前位置与起始位置的差值state.canvasState.xstate.panStartCanvasX(e.clientX-state.panStartX);state.canvasState.ystate.panStartCanvasY(e.clientY-state.panStartY);// 保证样式更新在浏览器的最佳渲染帧执行requestAnimationFrame((){DOM.canvasInner.style.transformtranslate(${state.canvasState.x}px,${state.canvasState.y}px) scale(${state.canvasState.scale});});}});滚轮缩放同理通过监听wheel事件增减state.canvasState.scale的值并限制在0.25到2.0的合理范围内。九、 组件物料注册表 (Registry)在添加诸如“国家选择”、“单选框”等复杂组件时它们可能带有预设的选项数组。系统通过统一的物料注册表进行管理实现了代码配置的分离。// components/registry.jsexportconstcomponentDefaults{input:{label:单行文本,placeholder:请输入内容,help:,defaultValue:},radio:{label:单选框组,help:,defaultValue:option_1,layout:inline,options:[{label:选项一,value:option_1},{label:选项二,value:option_2}]}// ... 可以通过追加对象无限扩展新物料};这个注册表不仅为createComponentDOM提供了挂载依据也被导出逻辑读取。十、 表单代码反编译 (Code Generation)低代码引擎最终需要输出代码供研发使用。本项目实现了一个简易的代码生成器通过递归遍历state.pages数据模型生成标准的 UI 代码片段。// ui/preview.jsfunctionbuildReactComponentCode(schema){letcodeimport React, { useState } from react;\n\n;codeexport default function DynamicForm() {\n;codeconst [formData, setFormData] useState({});\n\n;codeconst handleSubmit (e) { e.preventDefault(); console.log(formData); };\n\n;codereturn (\n form onSubmit{handleSubmit} classNameform-container\n;// 遍历每一个画布 / 页面schema.pages.forEach(page{codediv classNameform-page-card id${page.id}\n;codeh2${page.title}/h2\n;// 遍历页面内的所有组件page.elements.forEach(elementData{codegenerateJSXForElement(elementData);});code/div\n;});codebutton typesubmitSubmit/button\n;code/form\n );\n}\n;returncode;}// 根据 type 拼接具体的输入框 JSXfunctiongenerateJSXForElement(elData){if(elData.typeinput){returndiv classNameform-item label${elData.label}/label input typetext placeholder${elData.placeholder} onChange{(e) setFormData({...formData,${elData.key}: e.target.value})} / /div\n;}// ... 补充其他类型return;}通过这一层转换逻辑复杂的树状 JSON 即可输出为便于开发人员直接复制使用的前端源码至此完成了从“可视化拖拽”到“最终代码生产”的闭环。十一、 项目持续演进与未来路线在最初实现了基础的拖拽搭建与代码导出后我们从未停止对核心引擎的打磨。针对工业级低代码平台的标准我们已经或正在推进以下演进方向1. 状态管理与数据打通 (State Schema Sync) -【已实现】我们不仅修复了由于跨模板切换引发的“组件 ID 计数器泄露”漏洞通过在loadSchema中自适应扫描底层节点并重置最高水位线更实现了画布 DOM 树与底层 JSON Schema 的完美双向数据打通。任何在右侧面板的修改、画布的拖拽都会实时反馈至底层模型使预览模块可以直接对接真实 Schema 数据。未来的演进则考虑在“撤销/重做”栈中引入类似 Immutable 数据结构或简易 Diff 算法以降低内存开销。2. 组件联动与逻辑编排 (Logic Flow)目前的表单组件之间是彼此独立的。下一步将引入规则引擎Rule Engine支持组件间的值联动Value Linkage与显隐控制。例如当“国家选择”选中“中国”时“省市联动组件”才显示。这需要在schema中定义dependencies或expressions字段并在核心引擎层增加表达式解析模块。3. API 持久化与动态数据源接入 (Mock API Remote Data) -【已实现与演进中】我们已经抽离出了api.js层利用 Promise 和 localStorage 构建了一套完整的伪后端Mock API服务完美打通了表单的“发布”、“加载初始模板”等流程保证了页面刷新不丢数据。下一步演进是针对组件本身如下拉框、单选框支持绑定远程 API 获取选项数据允许配置请求 URL 和 Response 解析路径在画布预览时动态渲染异步数据。4. 插件化自定义物料 (Custom Plugin System)目前的所有组件在index.html的template中写死。后续计划开发插件注入系统允许开发者遵循特定规范编写独立的.js或.html组件包通过全局方法window.LowcodeEngine.registerComponent()在运行时动态将自定义业务组件注入到左侧物料区中。