简介这是一份基于Vue的可视化表单设计器完整源码面向需要快速构建复杂表单的前端开发者能用拖拽配置替代手写表单显著提升开发效率。源码核心包含可视化页面配置、栅格布局与flex对齐、一键预览效果、生成JSON数据以及一键生成可运行代码同时支持自定义组件、远端数据接口、高级组件、表单验证和国际化扩展基本覆盖了表单场景的常见需求。包内共437个文件以395个JavaScript文件为主另有12个Vue组件、样式表与字体图标、Markdown说明和示例页面整体仅2.51MB结构紧凑清晰便于阅读和二次开发。已有578人学习该资源适合有一定Vue基础并关注低代码方向的前端开发者可从中借鉴组件封装思路、Schema驱动渲染方式和完整工具链设计。1. 可视化表单设计器拖拽背后是一份可被解释的JSON当业务部门拿着Excel模板一周改三次字段来找你时你就明白表单的生命力不在于写死的HTML而在于可被解释的数据。可视化表单设计器要解决的就是让不写代码的人通过拖拽、配置、预览产出表单而开发者拿到的不是一段拼接字符串而是一份结构化的JSON。这个源码包把设计器本身做成了JavaScript实现前端渲染、后端校验、多端适配都可以由这份JSON驱动。文章只谈JS实现下从拖拽到渲染、从数据结构到接入业务系统的真实路径适合要自研低代码表单能力、或给现有后台管理系统加一个表单设计模块的开发者。核心先记住一句话先把数据结构想清楚界面才有得聊。2. 先建模再动手表单设计器的数据结构与JSON选型2.1 表单设计器和“拖拽生成代码”的本质区别很多团队第一次做表单设计器会把拖拽结果渲染成一段HTML字符串存数据库。结果往往是要么保存后再打开编辑时解析困难要么回显的HTML已经不可控。行业里更稳的做法是设计器输出一份统一的数据结构也就是JSON。运行时拿到JSON渲染成表单设计态把JSON渲染成可编辑的画布。这种设计最大的优势在于“三份东西共享同一份数据”设计器画布、运行时表单、后端校验规则都从同一份JSON出发。后端不是JavaScript也能消费Python侧有jsonschemaJava侧有everit连最终落库的字段约束也能对齐。设计器在生产工具链里的角色不过是这份JSON的可视化编辑器。JavaScript可视化表单设计器的技术含量其实大半都在这个数据模型的取舍上。2.2 表单节点type、key、props、rules的语义约定一份表单的顶层结构会包含表单标识和一组字段数组。这里用一个请假场景的schema做说明{ formKey: leave_apply, formName: 请假申请, fields: [ { type: input, key: employeeName, label: 姓名, props: { placeholder: 请输入姓名, maxlength: 20 }, rules: [ { required: true, message: 姓名必填 } ] }, { type: select, key: leaveType, label: 请假类型, props: { options: [ { label: 事假, value: personal }, { label: 病假, value: sick } ] }, rules: [ { required: true, message: 请选择请假类型 } ] } ] }每个字段节点的字段语义如下表字段名含义说明type组件类型决定渲染哪个组件对应组件注册表中的keykey字段名提交时作为JSON对象的键全局唯一label显示名表单左侧或上方的文案props组件属性placeholder、options等透传给实际组件rules校验规则required、pattern、min等由运行时翻译执行这份约定要足够固定否则设计器保存的和运行时读取的会不一致。另外建议在顶层加version字段数据模型升级时可以做migration。很多开源表单设计器的源码包里都有这个字段它解决的是老表单在迭代后的兼容问题。2.3 组件面板用元信息驱动三列布局组件面板看起来是一堆可拖拽的图标但每个图标背后是一份组件元信息widget meta元信息至少要包含类型、图标、默认属性和可配置项。设计器的标准布局是三列左侧组件库、中间画布、右侧属性配置。拖一个组件到画布本质是从元信息生成一个字段节点插入数据数组。常用组件的映射关系如下type映射组件关键props常见rulesinput单行输入placeholder、maxlengthrequired、patterntextarea多行输入rows、placeholderrequired、maxlengthnumber数字输入min、max、steprangeselect下拉选择options、multiplerequiredradio单选组options、verticalrequireddate日期选择format、valueFormatrequired这张表就是渲染引擎的路由表不管底层是React还是Vue最终都维护一个fieldMap把类型映射到组件实例。2.4 为什么不用HTML字符串而用JSON有人会问直接把HTML模板存库不行吗不行。HTML字符串做不了三件事编辑回填困难、校验规则无处安放、版本对比做不到。JSON可以diff、可以迁移、可以给后端直接消费。尤其当表单设计器需要支持联动、计算字段时HTML模板的表达能力更弱。这一层理解透了读源码时就不会被“JSON schema绕远路”这种问题干扰。3. JavaScript核心实现拖拽、选中回填与画布渲染3.1 HTML5拖放API从组件面板到画布的最小闭环浏览器原生支持HTML5 drag事件实现拖拽比监听mousemove简单。组件面板的每个卡片设置draggabletrue在dragstart里把组件类型写入dataTransfer。这里有几个细节getData只能同步读取类型要自定义MIME避免和文本内容混在一起dragend要清理拖拽时的样式class。palette.addEventListener(dragstart, (e) { const el e.target.closest([data-widget-type]); if (!el) return; e.dataTransfer.setData(widget/type, el.dataset.widgetType); e.dataTransfer.effectAllowed copy; el.classList.add(dragging); }); palette.addEventListener(dragend, (e) { e.target.classList.remove(dragging); });画布侧要监听dragover和drop。dragover必须preventDefault()否则drop不会触发drop时从dataTransfer中取类型调用createFieldByType生成字段节点再插入数组。canvas.addEventListener(dragover, (e) { e.preventDefault(); e.dataTransfer.dropEffect copy; }); canvas.addEventListener(drop, (e) { const type e.dataTransfer.getData(widget/type); if (!type) return; const field createFieldByType(type); state.fields.push(field); pushHistory(); render(); });createFieldByType的实现很直白从widget meta里深拷贝默认props用crypto.randomUUID()生成key再补齐label和rules。key不建议用时间戳或自增数字因为字段一旦被联动表达式引用删除和重排会产生脏引用。这里把落点简化为插入到数组末尾真实产品里会按drop事件的clientY计算精确插入位置属于落点优化数据流本身不变。3.2 画布渲染数组驱动视图事件委托处理选中画布直接遍历state.fields渲染用activeKey标记当前选中项。事件处理采用委托避免给每个字段单独绑定click。Vue模板可以这样组织template div v-forfield in fields :keyfield.key :data-field-keyfield.key classfield-item :class{ field-item--active: field.key activeKey } clickactiveKey field.key component :isfieldMap[field.type] v-bindfield.props changehandleFieldChange(field.key, $event) / div v-iffield.key activeKey classfield-item__mask / /div /templatecomponent :isfieldMap[field.type]是Vue的动态组件写法相当于一个组件路由器。v-bindfield.props把定义好的props透传给具体组件。外层包了一个.field-item容器为选中框、拖拽手柄和错误提示预留空间。容器上加click做事件委托比在组件内部抛事件更可控。设计器模式下这个容器还要负责阻止点击事件冒泡到画布空白区域清空选中状态。3.3 属性面板回填选中到配置的数据闭环选中画布字段后右侧属性面板要显示该字段的配置。数据流是点击画布字段设置activeKey属性面板读取activeField修改后写回field.props。更新时要保持字段引用不丢不能整体替换数组导致焦点丢失。function updateActiveField(patch) { const field state.fields.find((f) f.key state.activeKey); if (!field) return; if (patch.props) { field.props { ...field.props, ...patch.props }; } if (patch.rules) { field.rules patch.rules; } pushHistory(); render(); }这里的patch.props在执行时要注意select组件的options数组是引用类型直接用展开运算符合并会导致旧数组被共享后续删除选项时出现脏数据。稳妥做法是对options做一次浅拷贝。很多源码包把updateActiveField放在每次change事件里触发输入框每敲一个字符画布就重渲染一次光标会被重置。这个问题要么用防抖要么在属性面板修改时跳过画布重渲染只更新右侧面板。设计器性能瓶颈大多出在这一步。4. 运行时表单JSON到提交数据的渲染、校验与联动4.1 运行时渲染器设计器产出的数据如何变成可用页面源码包真正接入业务系统时要用的不是编辑器本体而是运行时渲染器。它输入schema和values输出表单UI和校验结果。实现上比设计器简单遍历fields复用同一个fieldMap。const fieldMap { input: TextInput, textarea: TextareaInput, number: NumberInput, select: SelectWidget, radio: RadioGroup, date: DatePicker, }; export function buildRuntimeFields(schema, values) { return schema.fields.map((field) ({ ...field, component: fieldMap[field.type], value: values[field.key] ?? field.props.defaultValue ?? , })); }参数说明schema是设计器导出的JSONvalues是外部传入的表单值对象fieldMap把字符串type映射到组件。返回值是渲染所需的字段列表。业务层拿到这个列表后在渲染层循环输出即可。运行时和设计器复用fieldMap是判断源码包工程质量的关键标准。如果两套组件定义不一致会出现设计器预览正常、运行时缺组件的问题。4.2 校验规则落库把rules翻译成提交时的错误信息设计器里配置的rules只是数据运行时必须把它们翻译成可执行的校验逻辑。实现一个轻量校验器async function validateForm(schema, values) { const errors {}; for (const field of schema.fields) { const value values[field.key]; for (const rule of field.rules || []) { if (rule.required) { const isEmpty value null || String(value).trim() ; if (isEmpty) { errors[field.key] rule.message || 该字段必填; break; } } if (rule.pattern value ! null String(value) ! ) { const regex new RegExp(rule.pattern, rule.flags || ); if (!regex.test(String(value))) { errors[field.key] rule.message || 格式不正确; break; } } if (rule.min ! null Number(value) rule.min) { errors[field.key] rule.message || 最小值为 ${rule.min}; break; } } } return errors; }注意required和pattern不能在同一轮里继续往下执行必须break否则一个字段会被错误信息覆盖。空字符串、null、undefined三者的语义不同required对空字符串生效pattern只对非空值生效。如果不想手写校验器可以用async-validator它能直接消费数组式rules但手写在定制错误文案上更灵活。4.3 联动与表达式动态显隐的安全边界表单设计器做到一定程度业务方就会要求联动选完“请假类型”后“事假说明”才显示。落到数据模型上是在字段上增加一个visible配置function evaluateVisible(field, values) { if (!field.visible) return true; const { operator, target, value } field.visible; const result values[target] value; return operator notEquals ? !result : result; }这里用的是声明式联动配置只有operator、target、value三个字段。很多表单设计器会进一步发展成“动态执行脚本”允许管理员配置一段JavaScript表达式来控制显隐或计算字段值。常见实现是new Function(values, expression)但这条路径必须意识到有存储型XSS风险恶意脚本会在预览用户的管理端浏览器里执行。安全评审时这是必检项。防护手段从两个方向做配置来源限制为受信管理员表达式解析改为白名单方式只允许、、||、括号和有限属性访问不用new Function。这一点在接入若依这类后台框架时尤其重要因为后台管理员的会话权限本身就是高危目标。5. 源码包落地技巧目录结构、自定义组件与安全加固5.1 拿到源码包先看哪几个文件一个JavaScript可视化表单设计器的源码包通常自带构建工具。拿到手先看package.json的scripts确认是Vite还是Webpack工程再看src/widgets或src/components目录确认内置组件注册入口最后看入口文件里设计器和运行时是否分离。启动开发环境一般是npm install npm run dev。如果源码包不依赖构建流程也可以用HBuilderX一类编辑器直接把目录作为静态资源打开因为纯JavaScript实现的部分对运行环境不挑剔浏览器本身就能完成渲染。5.2 注册自定义组件让设计器和运行时同时认识新组件源码包会暴露一个registerWidget注册方法。扩展新组件的套路是固定的import { registerWidget } from /core/widget-registry; registerWidget(my-upload, { component: MyUpload, defaultProps: { placeholder: 请上传文件, maxCount: 3, }, configPanel: UploadConfigPanel, });三个参数缺一不可component是运行时渲染的组件defaultProps决定拖入画布时的初始状态configPanel是右侧属性面板的编辑组件。只注册component不注册configPanel会导致字段能拖入但不能配置属性这是自制组件最容易踩的坑。registerWidget内部会把元信息同时注入组件面板和运行时fieldMap。5.3 安全加固与性能的三个检查点第一禁止用v-html渲染用户输入的label和description表单配置可能来自管理员但内容要在渲染层转义。第二new Function执行动态脚本是存储型XSS高危点按照4.3节的方案替换成白名单解析。第三字段超过50个时画布全量渲染会造成输入卡顿解决方案是虚拟滚动或按需懒渲染。验证方法在设计器里输入一段XSS测试字符串导出JSON后在运行态页面确认它是否被当作文本渲染。整个源码包的最终验收绕不开这一条。本文还有配套的精品资源点击获取