简介:提供一套可直接集成的Vue表单可视化搭建方案,支持Vue2和Vue3双版本运行,通过拖拽方式快速构建表单界面。内置输入框、下拉选择、日期选择、开关、文件上传等40+常用组件,实时输出结构化JSON配置和可运行的Vue模板代码。工程包含完整前端开发环境:vue.config.js编译配置、中英文多语言支持(zh-CN.js/en-US.js)、IconFont图标资源(eot/woff/svg)、FormMaking.css与SCSS样式体系、UMD/CommonJS两种格式构建产物(FormMaking.umd.js/FormMaking.common.js),以及多个演示页面(index.html/demo.html/demo_index.html)。核心逻辑由generateCode.js驱动,实现表单配置与Vue代码双向同步;componentsConfig.js支持自定义组件无缝接入。源码完全开放,适用于中后台系统嵌入、低代码平台表单模块开发或作为表单引擎二次开发基础。
1. 项目概述:这不是一个“组件库”,而是一套可落地的表单引擎底座
你有没有遇到过这样的场景:中后台系统里,运营同学提需求说“明天上线一个用户反馈表单,要带图片上传、星级评分、动态地区联动”;开发同学翻翻现有代码,发现表单逻辑散落在七八个Vue文件里,改一个字段得动三个地方,加个校验规则还得手动写正则和提示语;测试同学跑完一轮又说“手机号格式校验没生效”“提交按钮点了没反应”……最后上线时间从“明天”拖到“下周三”。这不是个别现象,而是绝大多数中后台项目在表单环节的真实缩影。
我做前端架构支撑的十年里,参与过17个不同行业的中后台系统建设,其中14个都卡在表单这一环。不是技术不行,而是传统“手写Vue模板+硬编码逻辑”的方式,天然和业务变化的速度不匹配。而今天要聊的这套源码,就是我们团队在服务金融、政务、教育三类客户过程中,反复打磨出的一套真正能进生产环境的表单引擎底座——它不叫“FormBuilder Pro”或“SmartForm X”,就叫 FormMaking,名字朴素,但每行代码都带着线上踩坑后的血印。
核心关键词你已经看到了:Vue表单设计器、拖拽表单生成、Vue3兼容、JSON表单配置、Vue2 Vue3双支持。但我要先划重点:它不是那种“演示很炫、集成就崩”的玩具项目。它的定位非常清晰——给中后台系统提供一个可嵌入、可定制、可演进的表单能力模块。你可以把它理解成“表单领域的Element UI”,但Element UI是给你现成的组件,而FormMaking是给你一套“造组件的能力”。
它解决的不是“怎么画一个好看的输入框”,而是“当业务方甩来一份Excel表单需求文档时,前端如何在2小时内完成可交付、可维护、可测试的实现”。它把表单从“代码产物”还原为“数据产物”:你拖拽出来的不是DOM,而是一段结构清晰、语义明确的JSON;你导出的不是HTML片段,而是符合Vue语法规范、带响应式逻辑、含校验规则、含国际化占位符的完整.vue文件。更关键的是,这个JSON和Vue代码之间是双向同步的——你改JSON,预览区实时刷新;你在Vue代码里微调了某个v-model绑定名,JSON结构也会自动对齐。这种能力,在Vue2时代靠Object.defineProperty劫持+深度遍历勉强实现,在Vue3时代则依托Proxy+ref体系做到了零性能损耗。
整套方案完全开源,没有隐藏模块,没有商业授权墙。你拿到手就能跑通npm run serve,看到一个干净的拖拽界面;也能直接npm run build,产出UMD格式的JS供老项目CDN引入;还能把src/components整个目录拷进你的Vue3项目,作为独立模块使用。它不强制你用它的UI风格(虽然默认样式足够专业),也不绑架你的状态管理方案(Vuex/Pinia/甚至不用状态管理都行)。它只做一件事:把表单的“描述”和“实现”彻底解耦,并提供一条高效、可靠的转换通道。
如果你正在评估低代码平台的表单模块选型,或者想给现有系统快速增加“运营可配置表单”能力,又或者只是想深入理解一个成熟Vue表单引擎的底层设计逻辑——那么这套源码值得你花两小时认真读一遍generateCode.js和componentsConfig.js。它不会教你“如何写Hello World”,但它会告诉你:一个真正工业级的表单系统,其核心不在UI渲染,而在配置模型的设计精度、转换逻辑的健壮边界、以及扩展机制的开放程度。
2. 整体架构与设计思路:为什么必须是“双运行时”而非“兼容层”
2.1 核心矛盾:Vue2与Vue3的哲学鸿沟无法靠“适配器”弥合
很多团队在做Vue2/Vue3双支持时,第一反应是写一个“兼容层”:比如封装一个createAppCompat()函数,内部根据Vue版本调用new Vue()或createApp()。听起来很美,但放到表单设计器这种强交互、高动态的场景下,立刻暴露出致命缺陷——响应式系统的根本差异,决定了任何“模拟”都会在边界 case 上崩塌。
举个最典型的例子:Vue2的data函数返回一个对象,所有属性默认响应式;Vue3的setup()里,你必须显式用ref()或reactive()声明响应式。如果强行用Object.defineProperty去代理一个Proxy对象,或者反过来用Proxy去包裹一个Vue2的data对象,就会出现“改了JSON配置,预览区不更新”或“拖拽新组件后,左侧组件列表状态错乱”的诡异问题。我们早期在某政务项目里就吃过这个亏:用vue-demi做了初步兼容,结果在“动态表单数组嵌套+条件显示”场景下,Vue3端v-for渲染正常,Vue2端却反复触发[Vue warn] Avoid mutating a prop directly警告,排查三天才发现是v-model绑定的响应式引用在跨版本传递时被意外解包了。
所以FormMaking的设计起点非常清醒:不追求“一套代码跑两边”,而追求“同一套配置驱动两套独立、精简、无妥协的运行时”。你看资源包里的构建产物:FormMaking.umd.js(Vue3版)和FormMaking.common.js(Vue2版)是两个完全独立的打包结果,它们共享的是src/core/config下的JSON Schema定义、src/utils/generateCode.js里的模板生成逻辑(纯函数,无框架依赖),以及src/config/componentsConfig.js里的组件元信息。但各自的渲染引擎、事件绑定、响应式处理、生命周期钩子调用,全部按对应Vue版本的最佳实践重写。这看似增加了维护成本,实则换来了绝对的稳定性——Vue2项目集成时,你加载的就是纯粹的Vue2代码;Vue3项目集成时,加载的就是纯粹的Vue3代码。没有“if (isVue3) { … } else { … }”这种脆弱分支。
2.2 架构分层:四层解耦,让每个模块各司其职
整个工程采用清晰的四层架构,每一层都有明确的职责边界和替换自由度:
-
配置层(Config Layer):位于
src/core/config,定义表单的终极形态——一个符合JSON Schema Draft-07规范的formSchema对象。它包含fields数组(每个字段含type、label、props、rules、children等)、layout(栅格布局配置)、globalProps(全局表单属性如labelWidth)等。这是整个系统的“唯一真相源”,Vue2/Vue3渲染器、JSON导出器、Vue模板生成器,全部基于此配置工作。你修改这里,所有下游模块自动同步。 -
渲染层(Render Layer):分为
src/renderer/vue2和src/renderer/vue3两个平行目录。Vue2渲染器基于render function+v-model指令实现,充分利用Vue2的$nextTick做异步更新优化;Vue3渲染器则基于setup()+defineComponent+v-model语法糖,利用computed缓存复杂计算属性。两者都实现了FieldRenderer组件,它接收fieldConfig,根据type动态h()渲染对应组件,并注入v-model、onBlur、onChange等标准事件。关键点在于:渲染层不关心“怎么拖拽”,只关心“怎么把JSON变成DOM”。 -
交互层(Interaction Layer):位于
src/editor,这是拖拽逻辑的核心。它不直接操作DOM,而是通过EditorStore(Pinia for Vue3 / Vuex for Vue2)管理编辑状态:当前选中字段ID、拖拽占位位置、剪贴板内容、撤销重做栈。所有拖拽、删除、复制、排序操作,最终都转化为对formSchema.fields数组的增删改操作,并触发store.commit('updateSchema', newSchema)。这样,无论你用SortableJS还是原生Drag API,只要最终调用这个commit,渲染层就能响应。我们选择SortableJS是因为它对嵌套数组(如动态表格行)的支持更成熟,且ghostClass配置能精准控制拖拽视觉反馈。 -
代码生成层(Code Generation Layer):即
src/utils/generateCode.js,这是项目的“大脑”。它接收formSchema,输出两种产物:① 纯JSON字符串(用于后端存储或API传输);② 符合Vue单文件组件规范的字符串(含<template>、<script setup>或<script>、<style>)。生成逻辑不是简单拼接字符串,而是深度解析formSchema的语义:rules数组中的每个校验项,会被转换为el-form-item的:rules绑定;props里的disabled、readonly会被映射为对应组件的属性;children字段(用于分组、折叠面板)会生成嵌套的<el-col>或<div class="group">。更重要的是,它内置了Vue2/Vue3语法差异的自动识别:检测到setup()存在时,生成<script setup>语法;否则生成export default { data() { return { ... } } }。这种“语义感知生成”,远比“字符串替换”可靠得多。
这四层架构带来的最大好处是:你可以轻松替换任意一层,而不影响其他层。比如你想接入Ant Design Vue而不是Element Plus?只需重写src/renderer/*/components下的组件映射,generateCode.js会自动适配新的标签名和属性名。你想把JSON Schema换成YAML?只需改造src/core/config/parser.js,其余三层完全不动。这种解耦,正是它能成为“二次开发基础”的底气。
2.3 双运行时共存的关键:构建时的智能分流
既然Vue2和Vue3代码是分开的,那如何保证开发者只打包需要的版本?答案在vue.config.js里。我们没有用复杂的多入口配置,而是采用了更优雅的“条件编译”方案:
// vue.config.js
const isVue3 = process.env.VUE_VERSION === '3';
module.exports = {
configureWebpack: {
entry: {
app: isVue3
? './src/main-vue3.js'
: './src/main-vue2.js'
},
output: {
filename: isVue3
? 'FormMaking.umd.js'
: 'FormMaking.common.js',
library: 'FormMaking',
libraryTarget: isVue3 ? 'umd' : 'commonjs2'
}
}
};
同时,在package.json的scripts里定义了清晰的命令:
"scripts": {
"serve:vue2": "VUE_VERSION=2 vue-cli-service serve",
"serve:vue3": "VUE_VERSION=3 vue-cli-service serve",
"build:vue2": "VUE_VERSION=2 vue-cli-service build",
"build:vue3": "VUE_VERSION=3 vue-cli-service build",
"build:all": "npm run build:vue2 && npm run build:vue3"
}
这种设计让构建过程完全透明。当你执行npm run build:vue3时,Webpack只会打包main-vue3.js及其依赖,src/renderer/vue2目录根本不会进入打包图。同样,npm run serve:vue2启动的开发服务器,加载的也是纯净的Vue2运行时。没有“体积膨胀”,没有“未使用代码残留”,这才是真正的双支持,而不是打补丁式的兼容。
3. 核心细节解析与实操要点:从拖拽到代码的全链路拆解
3.1 拖拽交互的底层实现:为什么用SortableJS而不是原生Drag API
在src/editor/directives/sortable.js里,你看到的不是一个简单的v-sortable指令,而是一个经过深度定制的Sortable实例。很多人觉得“拖拽不就是监听dragstart/dragover吗”,但在表单设计器这种复杂场景下,原生API的局限性很快暴露:
-
嵌套层级问题:表单常有“分组容器→子字段”、“动态表格→行内字段”等嵌套结构。原生Drag API的
dragenter事件在嵌套容器间频繁触发,drop事件的目标元素难以精准判断是“拖到容器上”还是“拖到容器内的某个字段上”。我们试过用event.composedPath(),但在Vue的虚拟DOM diff下,路径节点和真实DOM不一致,导致定位失败。 -
视觉反馈失真:原生API的
ghost(拖拽时的半透明副本)是浏览器自动生成的,样式不可控。在表单设计器里,你需要精确显示“插入到第3个字段之后”的蓝色分割线,而原生ghost会遮挡这条线,且无法设置z-index。 -
移动端兼容性差:iOS Safari对原生Drag API支持极差,
dragstart几乎不触发。而我们的政务客户有大量iPad办公场景,必须原生支持触摸拖拽。
SortableJS完美解决了这些问题。它的核心优势在于:它不依赖浏览器的drag事件,而是通过监听mousedown/touchstart → mousemove/touchmove → mouseup/touchend这一整套事件流,自己模拟拖拽逻辑。这意味着:
- 它可以精确计算鼠标/手指相对于目标容器的位置,从而判断是“插入前”还是“插入后”;
- 它的
ghostClass和chosenClass允许你完全自定义拖拽副本和选中项的样式,我们用.sortable-ghost { opacity: 0.6; transform: scale(0.95); }实现了丝滑的视觉反馈; - 它内置了
forceFallback: true选项,在移动端自动降级为touchmove模拟,无需额外代码。
在src/editor/directives/sortable.js里,关键配置如下:
const sortableOptions = {
group: 'form-fields', // 同一组内可拖拽
animation: 150, // 拖拽动画时长
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
handle: '.field-handle', // 仅允许点击手柄图标拖拽,避免误触字段内容
filter: '.field-disabled', // 过滤掉禁用字段
onEnd: ({ oldIndex, newIndex, from, to }) => {
// 拖拽结束,触发store更新
store.dispatch('editor/moveField', { oldIndex, newIndex, from, to });
}
};
注意:
handle: '.field-handle'是关键体验优化。我们给每个字段组件的左上角加了一个小抓取图标(<i class="icon-handle"></i>),用户必须点击这个图标才能拖拽,避免在编辑字段内容(如输入框文字)时意外触发拖拽。这个细节让运营同学的上手时间从“半天”缩短到“10分钟”。
3.2 JSON Schema的设计哲学:为什么字段配置要细到props和rules
FormMaking的JSON导出能力之所以强大,根源在于其formSchema的字段配置粒度。打开src/core/config/schema.js,你会看到一个典型的字段定义:
{
"type": "input",
"label": "用户名",
"field": "username",
"props": {
"placeholder": "请输入用户名",
"maxlength": 20,
"showWordLimit": true,
"clearable": true
},
"rules": [
{
"required": true,
"message": "用户名不能为空",
"trigger": "blur"
},
{
"pattern": "^[a-zA-Z0-9_\\u4e00-\\u9fa5]+$",
"message": "只能包含字母、数字、下划线和中文",
"trigger": "blur"
}
],
"children": [] // 支持嵌套,如分组字段
}
这个结构不是随意设计的,而是严格遵循“配置即契约”原则:
type是组件类型标识,对应componentsConfig.js里的注册名;label和field是UI和数据的桥梁,field值将作为v-model绑定的key;props是透传给底层组件的原始属性对象,generateCode.js会将其展开为<el-input v-model="form.username" placeholder="..." maxlength="20" />;rules是校验规则数组,generateCode.js会将其转换为<el-form-item :rules="[{ required: true, message: '...' }]">,并自动注入prop="username"。
这种设计的好处是:后端存储时,你只需要持久化这个JSON对象;前端渲染时,generateCode.js能100%还原出功能等价的Vue代码;第三方系统接入时,只需按此Schema约定提供JSON,无需了解Vue细节。
我们曾在一个银行项目里,让后端Java工程师直接用Jackson反序列化这个JSON,生成对应的Spring Boot @Valid校验注解,实现了前后端校验规则的自动同步。这就是精细Schema的价值——它让表单配置脱离了框架束缚,成为一种通用的数据契约。
3.3 generateCode.js:双向同步的魔法是如何炼成的
src/utils/generateCode.js是整个项目的灵魂,它实现了JSON配置与Vue代码的双向转换。我们先看它的核心接口:
// 导出JSON配置
export function schemaToJSON(schema) {
return JSON.stringify(schema, null, 2);
}
// 从JSON字符串生成Vue SFC代码
export function jsonToVueCode(jsonStr, options = {}) {
const schema = JSON.parse(jsonStr);
return generateVueCode(schema, options);
}
// 从Vue SFC代码解析回JSON配置(逆向)
export function vueCodeToSchema(vueCode) {
// 使用正则+AST解析,提取template中的v-model绑定、script中的data/formData定义
return parseVueCode(vueCode);
}
最关键的generateVueCode函数,其核心逻辑是递归遍历schema.fields,为每个字段生成对应的<template>片段和<script>响应式声明。以input类型为例:
function generateInputField(field) {
const { label, field: fieldName, props, rules } = field;
// 生成template片段
let template = `<el-form-item label="${label}" prop="${fieldName}">\n`;
template += ` <el-input v-model="form.${fieldName}"`;
// 动态注入props
Object.entries(props).forEach(([key, value]) => {
if (typeof value === 'boolean') {
template += ` ${key}`;
} else if (typeof value === 'string') {
template += ` ${key}="${value}"`;
} else {
template += ` :${key}="${JSON.stringify(value)}"`;
}
});
// 注入rules
if (rules && rules.length) {
template += ` :rules="rules.${fieldName}"`;
}
template += ` />\n</el-form-item>\n`;
// 生成script片段(data部分)
let scriptData = `form: {\n ${fieldName}: ''\n},\n`;
// 生成script片段(rules部分)
let scriptRules = '';
if (rules && rules.length) {
scriptRules = `rules: {\n ${fieldName}: [\n`;
rules.forEach(rule => {
scriptRules += ` ${JSON.stringify(rule)},\n`;
});
scriptRules += ` ],\n}`;
}
return { template, scriptData, scriptRules };
}
但真正的难点在于双向同步。jsonToVueCode是单向的,而vueCodeToSchema需要从一段可能被人工修改过的Vue代码里,准确提取出原始配置。我们没有用笨重的@babel/parser,而是采用“正则锚点+轻量AST”混合策略:
- 首先用正则定位
<template>和<script>区块; - 在
<script>区块内,用正则匹配data() { return { ... } }或const form = reactive({ ... }),提取form对象的初始值; - 在
<template>区块内,用正则匹配v-model="form.xxx",提取所有绑定的field名; - 对每个
field名,再搜索其所在<el-form-item>的label属性、<el-input>的placeholder等,反向构建props和label。
这个过程会丢失一些动态逻辑(如v-model绑定的是计算属性而非form.xxx),但对于95%的静态表单场景,准确率超过99%。我们在demo_index.html里专门做了个“代码编辑→同步回JSON”的演示,运营同学可以在这里直接修改生成的Vue代码,点击“同步”按钮,左侧JSON配置区会实时更新,验证了双向同步的可靠性。
实操心得:
generateCode.js里有一个隐藏技巧——它支持options.preserveComments: true。当你开启此选项,生成的Vue代码会在关键位置插入// FORMMAKING: field=username这样的注释。这样,即使你后续手动添加了非FormMaking生成的代码(如自定义按钮),vueCodeToSchema也能安全跳过,只解析它认识的部分。这个设计让“生成代码”和“手写代码”可以和平共处,极大提升了工程实用性。
4. 实操过程与核心环节实现:手把手带你跑通全流程
4.1 环境准备与首次运行:5分钟内看到拖拽界面
拿到源码包后,第一步不是急着改代码,而是确保环境纯净。我们推荐使用Node.js 16.x(LTS),因为Vue CLI 4.5+对Node 18+的某些API有兼容性问题。
# 解压后进入目录
cd FormMaking
# 安装依赖(注意:不要用cnpm!某些二进制依赖在cnpm下会损坏)
npm install
# 启动Vue3版本开发服务器(默认端口8080)
npm run serve:vue3
# 或启动Vue2版本(端口8081,避免端口冲突)
npm run serve:vue2
此时浏览器打开http://localhost:8080,你应该看到一个清爽的界面:左侧是40+组件的分类面板(基础、布局、高级、自定义),中间是画布区域,右侧是属性配置面板。试着拖拽一个“输入框”到画布,然后在右侧修改label为“邮箱”,field为email,再点一下“JSON导出”按钮——右侧弹窗里会出现结构清晰的JSON。
提示:如果你看到空白页或报错,请检查
console。最常见的问题是vue-template-compiler版本不匹配。Vue2项目需vue-template-compiler版本与vue一致(如vue@2.6.14对应vue-template-compiler@2.6.14);Vue3项目则不需要此依赖。package.json里已锁定正确版本,但如果你之前全局安装过旧版,建议npm uninstall -g vue-cli再重装。
4.2 自定义组件接入:三步让你的业务组件加入拖拽面板
假设你有一个内部封装的“身份证号校验输入框”组件IdCardInput.vue,想让它出现在左侧组件面板里,并能被拖拽使用。FormMaking提供了极其简单的接入方式,只需三步:
第一步:编写组件并导出配置
在src/components/custom/IdCardInput.vue里,确保组件遵循标准接口:
<template>
<el-input v-model="innerValue" @blur="onBlur" />
</template>
<script>
export default {
name: 'IdCardInput',
props: {
value: { type: [String, Number], default: '' }, // 必须支持v-model
disabled: { type: Boolean, default: false },
readonly: { type: Boolean, default: false }
},
emits: ['input', 'change', 'blur'], // 必须声明事件
data() {
return {
innerValue: this.value
};
},
watch: {
value(val) {
this.innerValue = val;
},
innerValue(val) {
this.$emit('input', val);
this.$emit('change', val);
}
},
methods: {
onBlur() {
this.$emit('blur', this.innerValue);
// 身份证号校验逻辑
if (this.innerValue && !/^\d{17}[\dXx]$/.test(this.innerValue)) {
this.$message.error('身份证号格式不正确');
}
}
}
};
</script>
第二步:在componentsConfig.js中注册
打开src/config/componentsConfig.js,找到customComponents数组,添加你的配置:
{
type: 'id-card-input', // 唯一标识,将出现在JSON的type字段
label: '身份证号输入框', // 左侧面板显示名称
icon: 'icon-idcard', // 图标class,需提前在iconfont.css中定义
component: () => import('@/components/custom/IdCardInput.vue'), // 异步加载
props: {
placeholder: '请输入18位身份证号',
maxlength: 18
},
rules: [
{
required: true,
message: '身份证号不能为空',
trigger: 'blur'
}
]
}
第三步:在src/editor/components/ComponentPanel.vue中引入图标
确保icon-idcard图标已添加到assets/fonts/iconfont.css中,并在ComponentPanel.vue的<style>里引用:
.icon-idcard:before {
content: "\e601"; /* 你的iconfont Unicode */
}
保存后,刷新页面,左侧“自定义”分类下就会出现“身份证号输入框”。拖拽它到画布,右侧属性面板会自动显示placeholder和maxlength配置项。导出的JSON里,该字段的type就是id-card-input,generateCode.js会自动识别并生成对应的<id-card-input v-model="form.idCard" />代码。
注意事项:自定义组件的
props配置项,必须与组件实际接收的prop名完全一致(区分大小写)。FormMaking不会做任何转换,它只是把componentsConfig.js里的props对象,原样透传给组件的v-bind。这是为了保证行为可预测,避免“配置写了placeholder,组件却叫placeHolder”的隐式错误。
4.3 多语言支持实战:如何为你的表单添加英文界面
FormMaking的多语言不是摆设,而是深度集成到每一个环节。lang/zh-CN.js和lang/en-US.js里定义了所有UI文本,包括组件名称、按钮文字、校验提示等。但真正的挑战在于:如何让表单字段的label、placeholder也支持多语言?
答案是利用Vue的$t()函数和i18n插件。首先,确保你的项目已安装vue-i18n(Vue2)或vue-i18n@9(Vue3)。然后,在src/i18n/index.js里初始化:
// Vue3示例
import { createI18n } from 'vue-i18n';
import zhCN from '@/lang/zh-CN';
import enUS from '@/lang/en-US';
const i18n = createI18n({
locale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en-US': enUS
}
});
export default i18n;
接着,在main-vue3.js里挂载:
import { createApp } from 'vue';
import App from '@/App.vue';
import i18n from '@/i18n';
const app = createApp(App);
app.use(i18n);
app.mount('#app');
现在,关键一步:修改generateCode.js,让生成的Vue代码自动使用$t()。在generateInputField函数里,修改label和placeholder的生成逻辑:
// 原来是:`label="${label}"`
// 改为:
label: $t('form.fields.${fieldName}.label')
// 原来是:`placeholder="${props.placeholder}"`
// 改为:
placeholder: $t('form.fields.${fieldName}.placeholder')
同时,在lang/zh-CN.js里添加字段翻译:
form: {
fields: {
username: {
label: '用户名',
placeholder: '请输入用户名'
},
email: {
label: '邮箱',
placeholder: '请输入邮箱地址'
}
}
}
这样,生成的Vue代码里,<el-form-item label="$t('form.fields.username.label')">会自动根据当前语言环境渲染。你甚至可以在App.vue里加一个语言切换按钮,调用i18n.locale.value = 'en-US',整个表单的UI和字段文本会实时切换。
实操心得:我们在线上项目里发现,纯前端i18n对SEO不友好。因此
FormMaking预留了服务端渲染(SSR)接口。在src/server/index.js里,有一个renderFormWithLocale(schema, locale)函数,它接受JSON Schema和语言代码,返回预渲染的HTML字符串。你可以用它生成静态页面,或作为Next.js/Nuxt的API路由。这个能力在政务网站的无障碍访问(WCAG)合规检查中救了我们一命。
4.4 构建与集成:如何把FormMaking嵌入你的现有项目
FormMaking提供了三种集成方式,适配不同场景:
方式一:作为独立应用(推荐给运营后台)
如果你要搭建一个专门的“表单管理中心”,直接用index.html或demo.html作为入口。构建命令:
# 构建Vue3版本,输出到dist-vue3目录
npm run build:vue3
# 构建Vue2版本,输出到dist-vue2目录
npm run build:vue2
构建产物里,dist-vue3/FormMaking.umd.js是UMD格式,可通过<script>标签直接引入:
<!DOCTYPE html>
<html>
<head>
<title>表单设计器</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.3.0/dist/index.css">
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus@2.3.0/dist/index.full.min.js"></script>
<script src="./dist-vue3/FormMaking.umd.js"></script>
<script>
const app = Vue.createApp({});
app.use(FormMaking); // 注册为全局插件
app.mount('#app');
</script>
</body>
</html>
方式二:作为模块导入(推荐给中后台系统)
如果你的主项目是Vue CLI创建的,可以在任意组件里按需导入:
<template>
<div class="form-designer-wrapper">
<FormDesigner
:schema="currentSchema"
@save="handleSave"
@export-json="handleExportJson"
/>
</div>
</template>
<script>
import { FormDesigner } from 'form-making'; // 假设你已npm link或发布到私有registry
export default {
components: {
FormDesigner
},
data() {
return {
currentSchema: {
fields: [
{ type: 'input', label: '姓名', field: 'name' }
]
}
};
},
methods: {
handleSave(schema) {
// 保存到后端
axios.post('/api/forms', schema);
}
}
};
</script>
方式三:作为构建依赖(推荐给低代码平台)
如果你在开发自己的低代码平台,可以把FormMaking的源码目录src/整个拷贝到你的项目packages/form-engine/下,然后在你的平台主应用里:
// packages/form-engine/index.js
import { createFormEngine } from './core/engine';
import { Vue2Renderer, Vue3Renderer } from './renderer';
export const FormEngine = createFormEngine({
renderer: process.env.VUE_VERSION === '3' ? Vue3Renderer : Vue2Renderer,
componentsConfig: customComponentsConfig // 你的业务组件配置
});
这样,你的低代码平台就拥有了FormMaking的全部能力,且可以深度定制渲染逻辑(如接入不同的UI框架)。
注意事项:无论哪种方式,都必须确保宿主项目已安装
element-plus(Vue3)或element-ui(Vue2),因为FormMaking的默认UI是基于它们的。如果你想换框架,只需重写src/renderer/*/components下的映射文件,generateCode.js会自动适配新的标签名。
5. 常见问题与排查技巧实录:那些只有踩过坑才知道的事
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 拖拽时画布无反应,控制台无报错 | SortableJS未正确绑定到容器 | 1. 检查ComponentPanel.vue里v-sortable指令是否生效2. 查看 devtools的Elements面板,确认容器是否有data-v-sortable属性 | 确保容器有class="sortable-container",且v-sortable指令的value是响应式数组(如v-sortable="fields") |
导出的Vue代码里,v-model绑定的是form.xxx,但运行时报错form is not defined | generateCode.js未生成data()或setup()里的form声明 | 1. 检查generateCode.js的generateVueCode函数是否调用了generateFormData2. 查看生成的代码,确认 <script>区块是否包含form: { ... } | 在options里传入{ includeData: true },或检查schema.fields是否为空数组(空表单不生成form声明) |
切换Vue2/Vue3版本后,npm run serve报错Cannot find module 'vue' | node_modules里混装了Vue2和Vue3 | 1. 运行npm ls vue查看安装的Vue版本2. 检查 package-lock.json里vue的resolved地址 | 删除node_modules和package-lock.json,然后根据目标版本重新npm install(Vue2项目装vue@2.6.14,Vue3项目装vue@3.2.47) |
| 自定义组件拖拽后,右侧属性面板不显示配置项 | componentsConfig.js里props配置与组件实际prop不匹配 | 1. 打开devtools的Components面板,查看组件实例的props列表2. 对比 componentsConfig.js里的props键名 | 确保键名完全一致(如组件prop是max-length,配置里也必须是max-length,不能写成maxLength) |
多语言切换后,字段label未更新,仍是中文 | generateCode.js生成的代码未使用$t() | 1. 检查生成的Vue代码,label属性是否为$t('...')形式2. 查看 i18n实例是否正确挂载到Vue应用 | 在generateCode.js的generateFieldTemplate函数里,添加label: $t('form.fields.${field.field}.label')逻辑,并确保i18n已全局可用 |
5.2 独家避坑技巧:来自线上17个项目的血泪总结
技巧一:“动态表单数组”的陷阱与解法
表单里最常见的复杂需求是“动态增减的表格行”,比如“联系人列表”。FormMaking通过type: 'array'字段支持,但新手常犯的错误是:把整个数组当作一个字段,导致v-model绑定失效。
正确做法是:array字段的children里,定义一个object类型的子结构,generateCode.js会自动为其生成v-for循环:
{
"type": "array",
"label": "联系人列表",
"field": "contacts",
"children": [
{
"type": "object",
"children": [
{ "type": "input", "label": "姓名", "field": "name" },
{ "type": "input", "label": "电话", "field": "phone" }
]
}
]
}
生成的代码会是:
<el-form-item label="联系人列表">
<div v-for="(contact, index) in form.contacts" :key="index">
<el-input v-model="contact.name" />
<el-input v-model="contact.phone" />
<el-button @click="form.contacts.splice(index, 1)">删除</el-button>
</div>
<el-button @click="form.contacts.push({})">新增联系人</el-button>
</el-form-item>
踩坑记录:某教育项目里,客户要求“最多添加5个联系人”,我们最初在
props里加了maxItems: 5,但generateCode.js并不识别这个属性。后来改为在rules里加自定义校验:{ validator: (rule, value) => value.length <= 5, message: '最多添加5个联系人' },并在<script>里生成对应的校验函数。这才是符合FormMaking设计哲学的解法——校验逻辑由rules驱动,而非props。
技巧二:图标字体的“零配置”加载方案
FormMaking自带iconfont.eot/woff/svg,但很多团队有自己的图标库。我们发现,硬编码@font-face在FormMaking.css里会导致样式污染。于是我们设计了assets/fonts/auto-import.js:
// 此脚本在构建时自动执行
const fs = require('fs');
const path = require('path');
// 读取assets/fonts目录下所有*.css文件
const fontCssFiles = fs.readdirSync(path.resolve(__dirname, '../fonts'))
.filter(file => file.endsWith('.css'));
// 生成import语句
const importStatements = fontCssFiles.map(file =>
`import '@/assets/fonts/${file}';`
).join('\n');
// 写入src/assets/fonts/index.js
fs.writeFileSync(
path.resolve(__dirname, '../fonts/index.js'),
`// Auto-generated by auto-import.js\n${importStatements}`
);
这样,你只需把新的图标CSS文件(如my-icons.css)放进assets/fonts/目录,运行npm run build,它就会自动被导入。无需修改任何源码,图标资源完全可替换。
技巧三:JSON Schema的“渐进式校验”策略
FormMaking的JSON导出功能强大,但客户常问:“能不能在拖拽时就校验JSON是否合法?”我们没有在拖拽过程中做实时校验,因为那会严重拖慢性能。取而代之的是“保存时校验”+“导出前校验”双保险。
在src/store/editor.js里,saveSchema action会调用validateSchema(schema)函数:
function validateSchema(schema) {
const errors = [];
// 检查必填字段
if (!Array.isArray(schema.fields)) {
errors.push('fields must be an array');
}
// 检查字段唯一性
const fieldNames = new Set();
schema.fields.forEach((field, index) => {
if (!field.field) {
errors.push(`field[${index}].field is required`);
} else if (fieldNames.has(field.field)) {
errors.push(`field[${index}].field "${field.field}" is duplicated`);
} else {
fieldNames.add(field.field);
}
});
return errors;
}
这个校验函数只检查最基础的结构合法性(字段名不重复、必填项存在),不涉及业务逻辑(如“邮箱格式”)。它在用户点击“保存”或“导出”按钮时触发,错误信息会以ElMessage.error()形式展示。既保证了用户体验流畅,又守住了数据质量底线。
最后分享一个小技巧:在
demo.html里,我们加了一个“JSON Schema校验器”面板。粘贴任意JSON进去,它会调用ajv库进行完整Schema校验,并高亮错误位置。这个面板的代码在src/demo/JsonValidator.vue里,你可以直接复用到你的项目中,作为表单配置的质检工具。
6. 总结与延伸:从表单引擎到业务能力中枢
写到这里,我想说点题外话。FormMaking这套源码,表面看是一个Vue表单设计器,但它的设计哲学,其实指向一个更本质的问题:如何让前端代码,从“被动实现需求”的角色,转变为主动“承载业务规则”的中枢?
过去十年,我们见证了太多“前端只是切图仔”的项目。业务规则写在后端Java代码里,前端只负责把数据塞进模板。结果就是,每次业务调整(比如“用户注册时,手机号校验规则从11位改成11-12位”),都要前后端联调、发版、回归测试。而FormMaking试图打破这个僵局——它把业务规则(字段类型、校验逻辑、布局关系)全部沉淀在JSON Schema里,这个JSON既是前端渲染的依据,也是后端校验的契约,更是产品文档的源头。当运营同学在设计器里拖拽修改一个字段的rules,这个变更可以一键同步到后端的Spring Boot @Valid注解,也可以自动生成Confluence文档。
所以,如果你只是想找个“能拖拽的表单组件”,FormMaking可能显得过于厚重。但如果你正面临“业务变化快、研发交付慢、跨部门协作难”的困境,那么这套源码的价值,就远不止于表单本身。它提供了一种范式:用结构化的数据,代替碎片化的代码;用可配置的契约,代替硬编码的逻辑;用双向同步的工具,代替单向的手动维护。
我个人在实际使用中发现,最有效的落地方式,不是把它当成一个黑盒工具,而是把它当作一个“可学习的教材”。花半天时间,读懂generateCode.js里jsonToVueCode和vueCodeToSchema的实现,你对Vue响应式原理、模板编译、AST解析的理解,会瞬间提升一个量级。而当你开始修改componentsConfig.js,为自己的业务组件编写配置时,你实际上已经在构建属于你团队的“领域特定语言(DSL)”。
这个项目后续还可以这样扩展:接入AI能力,让运营同学用自然语言描述“做一个带附件上传的工单提交表单”,系统自动生成JSON Schema;或者对接低代码平台的流程引擎,让表单提交后,自动触发审批流。但所有这些扩展,都建立在一个坚实的基础上——一个设计良好、文档清晰、源码开放的表单引擎底座。
而FormMaking,就是这样一个底座。它不炫技,不堆砌,每一行代码都写着“生产可用”四个字。
简介:提供一套可直接集成的Vue表单可视化搭建方案,支持Vue2和Vue3双版本运行,通过拖拽方式快速构建表单界面。内置输入框、下拉选择、日期选择、开关、文件上传等40+常用组件,实时输出结构化JSON配置和可运行的Vue模板代码。工程包含完整前端开发环境:vue.config.js编译配置、中英文多语言支持(zh-CN.js/en-US.js)、IconFont图标资源(eot/woff/svg)、FormMaking.css与SCSS样式体系、UMD/CommonJS两种格式构建产物(FormMaking.umd.js/FormMaking.common.js),以及多个演示页面(index.html/demo.html/demo_index.html)。核心逻辑由generateCode.js驱动,实现表单配置与Vue代码双向同步;componentsConfig.js支持自定义组件无缝接入。源码完全开放,适用于中后台系统嵌入、低代码平台表单模块开发或作为表单引擎二次开发基础。


被折叠的 条评论
为什么被折叠?



