把一个表单做到字段、校验、联动全可配置——我在真实项目里趟过的坑

之前做一个某服务包管理系统那阵子,我最怕产品跑过来说"这个表单再加两个字段"。不是怕写,是怕每次都要改代码、发版、等审核。车型几十个、配置项上百个,服务包的字段还跟着车型变——手写表单根本扛不住。

后来我们搞了一套"配置驱动"的表单:字段长什么样、怎么校验、字段之间怎么联动,全由一份 JSON 说了算。今天把这套东西拆开讲讲,顺便说说我踩过的坑,给遇到类似需求的同学省点时间。

第一版:堆 v-if 的笨办法

最早就是老老实实堆 el-form-item,字段少的时候还行。等到服务包分了"基础版 / 豪华版 / 定制版",每版的字段不一样,我就在 template 里套一层又一层的 v-if

<el-form-item label="定制说明" v-if="formModel.edition === 'custom'">
  <el-input v-model="formModel.customDesc" />
</el-form-item>

改一个字段要翻半天代码,最离谱的时候一个 .vue 文件 800 多行,自己都看吐了。更糟的是,产品每调一次配置我就得改一次代码,开发和发版节奏完全被表单绑架。

核心转变:schema 驱动

痛定思痛,我把"表单长什么样"从代码里抽出来,变成一份数据。一份 schema 描述所有字段:类型、标签、绑定的字段名、校验规则、下拉选项。渲染的时候遍历它,动态生成表单项。

const formSchema = [
  { type: 'input',   prop: 'packageName', label: '服务包名称',
    rules: [{ required: true, message: '服务包名称必填' }] },
  { type: 'select',  prop: 'carModel',    label: '适配车型',
    options: [{ label: '车型A', value: 'A' }, { label: '车型B', value: 'B' }] },
  { type: 'select',  prop: 'edition',     label: '版本',
    options: [], dependOn: 'carModel',
    watch: (val, formModel, ctx) => {
      // 车型变了,重新拉该车型下的版本列表
      formModel.edition = ''
      ctx.loadOptions('edition', val)
    } },
  { type: 'input',   prop: 'customDesc',  label: '定制说明',
    visibleWhen: (formModel) => formModel.edition === 'custom',
    rules: [(formModel) => ({
      required: formModel.edition === 'custom',
      message: '定制版必须填写定制说明'
    })] },
]

渲染引擎:字段类型映射到组件

关键其实就一个东西——字段类型到组件的映射表。Element UI 的 input / select / date-picker / switch 都有现成组件,用一个 map 兜住,再用 component :is 动态渲染。所有字段的值统一收集到一个 formModel 对象里。

<template>
  <el-form :model="formModel" :rules="mergedRules">
    <el-form-item
      v-for="field in visibleFields"
      :key="field.prop"
      :label="field.label"
      :prop="field.prop">
      <component
        :is="fieldMap[field.type]"
        v-model="formModel[field.prop]"
        v-bind="field.props || {}" />
    </el-form-item>
  </el-form>
</template>
​
<script>
import FormInput from './fields/FormInput.vue'
import FormSelect from './fields/FormSelect.vue'
// ...其他字段组件
​
export default {
  data() {
    return {
      formModel: {},
      fieldMap: { input: FormInput, select: FormSelect /* ... */ }
    }
  },
  computed: {
    visibleFields() {
      return this.schema.filter(f => !f.visibleWhen || f.visibleWhen(this.formModel))
    },
    mergedRules() {
      // 校验前先按当前 formModel 算出每个字段的 rules
      const rules = {}
      this.schema.forEach(f => {
        if (!f.rules) return
        rules[f.prop] = f.rules.map(r => (typeof r === 'function' ? r(this.formModel) : r))
      })
      return rules
    }
  }
}
</script>

这里有个 Vue2 的坑得提醒:动态 prop 的字段,如果不在 data 里预先声明,formModel[field.prop] 是不会响应式的。我当初就在这里栽过——表单能渲染,但输入不回显。解决办法是渲染前先 this.$set(this.formModel, field.prop, defaultValue),把所有 schema 里的字段一次性初始化好。

校验规则可配置,但"动态必填"才是坑

rules 本身就好配,Element 的 rules 数组天然是配置友好的。真正的麻烦是条件必填——比如选了"定制版"才要填"定制说明"。

我第一版是在 rules 里写死,结果加一个条件就改一处代码,又回到了原点。后来改成 rules 支持函数required 可以是 (formModel) => formModel.edition === 'custom'。引擎在每次校验前,先拿当前的 formModel 把每个字段的 rules 算一遍(见上面 mergedRulesmap)。这样"什么条件下必填"就彻底变成配置的事,不用动渲染代码。

联动逻辑:最难的那个

字段 A 一变,字段 B 的选项或显隐要跟着变。我第一版用 watch 硬编码 A→B 的关系,结果字段一多,watch 里套 watch,乱成一锅粥,哪天改了 A 忘了 B,bug 就来了。

后来统一在 schema 里给字段加 dependOnwatch 回调(见上面 edition 字段的例子)。引擎的逻辑是:任意字段变化后,遍历所有声明了 dependOn 该字段的字段,挨个调它们的 watch 回调。所有联动都走同一条调度通道,不再散落各处。新增一个联动,只改 schema,不改引擎。

真实项目里的几个边界问题

  • 字段上百个,渲染卡:用 v-show 替代频繁增删节点,或者按业务分 Tab / 分步骤。

  • 嵌套字段:服务包里常有"子配置列表",得支持数组型字段。schema 里加 type: 'array',渲染引擎碰到数组类型就递归渲染子 schema,数据结构也跟着递归收集。

  • 编辑态 vs 预览态:配置是同一份,只是渲染模式不同。编辑态把字段元信息(类型、选项)也展示出来,预览态只渲染纯表单。

回头看

这套东西现在想想,核心就一句话:把 UI 变成数据。配上 Vue 的 component :is 和动态 v-model,配置即表单。后来做别的系统遇到类似的"字段老变"的需求,直接把这套引擎搬过去、改改 schema 就行,开发和发版节奏再也没被表单拖过后腿。

给后来者的建议:别一上来就追求"万能表单",先把你们项目里字段变化最频繁的那一个表单做成 schema 驱动,尝到甜头再扩。真要做全了,你会发现难点从来不是"渲染",而是"联动"和"动态校验"这两块——把这两块用配置描述清楚,事情就成了一大半。

如果你们项目里也有类似的动态表单需求,欢迎在评论区聊聊你们是怎么解的,互相取取经。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妍思码匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值