复杂表单实战:我在项目里踩过的 Vue 坑
表单是前端最常见、也最容易写成一坨的东西。简单表单随便放几个 v-model 都能跑,一旦字段多起来、存在动态联动、跨步校验,代码就开始失控。
这篇文章基于我做过的一个"几十个字段 + 多步 + 条件显示"的表单项目,总结一些在 Vue 里踩过的坑和解决方案。
需求背景:一个略显「超标」的表单
当时的表单大概长这样:
- 3 个步骤:基础信息 → 业务信息 → 确认提交
- 每步有 8~15 个字段
- 部分字段需要「根据上一步选择动态显示或隐藏」
- 有跨字段校验(比如:结束日期 ≥ 开始日期)
- 需要支持「草稿保存」+「再次打开自动回填」
如果一股脑全写在一个组件里,大概会出现:
- data 里一个巨大的
form = { ... }。 - 各种
watch做联动逻辑。 - methods 里各种
validateXXX,互相依赖。 - 维护成本爆炸。
核心思路:拆数据结构 + 拆 UI + 拆校验
1. 数据结构要先定清楚
我一般会在一开始,就写出一个完整的 formModel:
const defaultForm = () => ({
basic: {
name: '',
idNo: '',
phone: '',
},
business: {
type: '',
amount: null,
startDate: '',
endDate: '',
},
extra: {
agree: false,
comment: '',
},
});
在组件里:
data() {
return {
form: defaultForm(),
};
}
这样做的好处:
- 字段分组一目了然(basic / business / extra)。
- 默认值清晰,重置时只需
this.form = defaultForm()。 - 后面多步拆组件的时候,很容易针对某一组传参。
2. UI 组件分步拆解
与其一口气在一个页面上写所有表单,不如每一步拆成一个子组件:
StepBasic.vueStepBusiness.vueStepExtra.vue
每个子组件只负责「渲染 + 局部简单校验」,数据透传上层统一管理。
父组件大致结构:
<template>
<div>
<StepBasic
v-if="currentStep === 1"
v-model="form.basic"
/>
<StepBusiness
v-if="currentStep === 2"
v-model="form.business"
/>
<StepExtra
v-if="currentStep === 3"
v-model="form.extra"
/>
<!-- 上一步 / 下一步 / 提交按钮 -->
</div>
</template>
子组件写法(以 StepBasic 为例):
<template>
<div>
<label>姓名</label>
<input v-model="localValue.name" />
<label>证件号</label>
<input v-model="localValue.idNo" />
</div>
</template>
<script>
export default {
name: 'StepBasic',
props: {
value: {
type: Object,
required: true,
},
},
data() {
return {
localValue: { ...this.value },
};
},
watch: {
localValue: {
deep: true,
handler(val) {
this.$emit('input', val);
},
},
value: {
deep: true,
handler(val) {
this.localValue = { ...val };
},
},
},
};
</script>
Vue 2 时代这种
v-model+localValue的写法挺常见。Vue 3 里可以用v-model自定义修饰符会更优雅,这里先不展开。
动态字段:不要「if 写死在模板里」
常见写法是:
<div v-if="form.business.type === 'A'">
<!-- 一坨字段 -->
</div>
<div v-if="form.business.type === 'B'">
<!-- 另一坨字段 -->
</div>
这会导致模板变得非常长,而且逻辑不好复用。
更推荐的是用配置驱动:
// fieldsConfig.js
export const businessFields = [
{
prop: 'amount',
label: '金额',
showWhen: (form) => true,
},
{
prop: 'discountRate',
label: '折扣率',
showWhen: (form) => form.business.type === 'A',
},
{
prop: 'extraInfo',
label: '补充说明',
showWhen: (form) => form.business.type === 'B',
},
];
组件里:
<div v-for="field in visibleFields" :key="field.prop">
<label>{{ field.label }}</label>
<input v-model="localValue[field.prop]" />
</div>
computed: {
visibleFields() {
return businessFields.filter(f => f.showWhen(this.formRoot)); // formRoot 指向整个 form
},
},
这样:
- 动态显示逻辑放在配置里,想改只改
showWhen。 - 也方便后面抽离出通用的「配置化表单组件」。
校验:用规则系统,而不是到处手写 if
以前我常在 submit 里写一堆:
if (!this.form.basic.name) { alert('请输入姓名'); return; }
if (!this.form.basic.idNo) { alert('请输入证件号'); return; }
if (!this.form.business.amount || this.form.business.amount <= 0) { ... }
后来发现,还是应该让校验也配置化。
假设用的是 Element UI 的 Form:
const rules = {
'basic.name': [
{ required: true, message: '请输入姓名' },
],
'business.amount': [
{ required: true, message: '请输入金额' },
{ type: 'number', min: 0.01, message: '金额必须大于 0' },
],
};
跨字段校验可以用自定义 validator:
'business.endDate': [
{
validator: (rule, value, callback) => {
const { startDate, endDate } = form.business;
if (endDate && startDate && endDate < startDate) {
callback(new Error('结束日期不能早于开始日期'));
} else {
callback();
}
},
trigger: 'change',
},
],
总结一下:
- 常规规则→配置化,尽量放到
rules对象里。 - 复杂逻辑→自定义 validator,用函数来组织逻辑,而不是散落在各个方法里。
草稿保存与回填
草稿保存是复杂表单的常见需求:用户填了一半离开,下次回来要恢复。
基本思路:
- 表单提交接口之外,增加「保存草稿」接口(或 localStorage 本地存储)。
- 草稿保存实际就是「把当前
form的 JSON 存起来」。 - 打开页面时请求草稿,如果有,就用草稿替代默认值。
伪代码:
methods: {
saveDraft() {
// 简单场景可以直接存 localStorage
localStorage.setItem('FORM_DRAFT', JSON.stringify(this.form));
},
loadDraft() {
const raw = localStorage.getItem('FORM_DRAFT');
if (raw) {
this.form = Object.assign(defaultForm(), JSON.parse(raw));
}
},
},
created() {
this.loadDraft();
}
注意:
- 回填时一定记得先用
defaultForm()打个底,再Object.assign,防止接口字段变更导致字段丢失。 - 如果是和后端交互草稿接口,建议在后端也做一次字段兜底。
我踩过的几个坑
-
在子组件直接修改 props
// 坑 props: ['value'], created() { this.value.xxx = '123'; // 违反单向数据流 }构造一个
localValue,通过v-model同步回父组件会更健康。 -
watch 里逻辑过多
如果你发现某个
watch函数超 30 行,那大概率说明逻辑该拆了,可以抽出成普通方法、甚至独立模块。 -
校验时混用了多套规则
有些字段既写了组件库的规则,也又手写了额外的
if判断,容易出现「一个地方改了校验,另一个地方忘记改」的问题。建议统一走一套规则体系。
总结
复杂表单本质是个状态管理 + UI 编排问题:
- 先把数据结构设计好(默认值、分组)。
- 再用组件拆分 UI(一步一个组件)。
- 动态展示和校验尽量配置化,不要写死在模板里。
- 共性逻辑抽出来,下一次做类似表单就会轻松很多。
表单不会消失,但写起来可以比「大 if 地狱」舒服得多。