动态表单设计解析

目录

前言

本文所涉及的表单以 Ant Design 为主,但不限于此,因为原理都是相通的。

一、动态表单的「可配置化」设计

本质问题:

  • 表单不再是页面,而是"数据驱动的渲染系统"

1、动态表单的本质

动态表单 = Schema → 渲染引擎 → 表单实例

typescript 复制代码
Schema(配置数据)
   ↓
Form Renderer(渲染引擎)
   ↓
Form State(值 / 校验 / 状态)

你要解决的不是:

  • "怎么用 Form.Item + Input"

而是:

  • "如何用一份 JSON 描述 100+ 表单形态"

2、表单 Schema 的核心设计

(1)、最小完备 Schema 结构

typescript 复制代码
interface FieldSchema {
  name: string
  label: string
  type: 'input' | 'select' | 'date' | 'custom'
  componentProps?: Record<string, any>

  // 表现
  visible?: boolean | Expression
  disabled?: boolean | Expression

  // 数据
  defaultValue?: any
  options?: Options | AsyncOptions

  // 校验
  rules?: RuleSchema[]

  // 联动
  reactions?: ReactionSchema[]
}

设计原则:

  • 表单项 = 纯数据
  • 不允许在 Schema 中写 UI 逻辑
  • 所有动态行为都"声明式"

(2)、组件注册机制(核心)

typescript 复制代码
const componentRegistry = {
  input: Input,
  select: Select,
  date: DatePicker,
  custom: CustomComponent
}

const Comp = componentRegistry[field.type]
return <Comp {...field.componentProps} />

这是动态表单可扩展性的根

3、表单联动 & 条件渲染(最难点之一)

❌ 错误方式(命令式):

typescript 复制代码
watch(a, (val) => {
  if (val === 1) {
    showB()
  }
})

✅ 正确方式(声明式 reaction):

typescript 复制代码
reactions: [
  {
    when: 'form.values.a === 1',
    fulfill: {
      state: {
        visible: true,
        required: true
      }
    },
    otherwise: {
      state: {
        visible: false
      }
    }
  }
]

表单引擎要做什么?

  • 建立 依赖图
  • 精确触发(只重算受影响字段)
  • 防止循环依赖

Ant Design Form / Formily / React Hook Form 本质都在做这件事

4、动态表单的工程分层(很关键)

typescript 复制代码
├── schema/
│   ├── fields.ts
│   ├── validators.ts
│   └── reactions.ts
├── renderer/
│   ├── FormRenderer.tsx
│   └── FieldRenderer.tsx
├── engine/
│   ├── state.ts
│   ├── validator.ts
│   └── effect.ts
  • Schema 不应该写在页面里
  • 这是很多项目后期崩盘的原因。

二、校验体系设计(字段级 / 表单级 / 业务级)

本质问题:

  • "什么时候校验?校验谁?失败了谁来兜底?"

1、三层校验的职责边界(非常重要)

层级 校验对象 举例 特点
字段级 单字段 必填 / 正则 同步、即时
表单级 多字段 密码一致 需要上下文
业务级 表单 + 外部状态 金额 / 风控 异步、后端依赖

2、字段级校验(Field Validation)

核心原则:

  • 纯函数
  • 无副作用
  • 不依赖其他字段
typescript 复制代码
{
  required: true,
  message: '必填'
},
{
  validator: (value) => value.length >= 6
}

⚠️ 不要在字段级校验中访问 form.values

3、表单级校验(Form Validation)

典型场景:

  • 密码 / 确认密码
  • 起止时间
  • 组合字段完整性
typescript 复制代码
function validateForm(values) {
  if (values.start > values.end) {
    return {
      end: '结束时间必须大于开始时间'
    }
  }
}

执行时机:

  • 提交前
  • 部分字段变更后(慎用)

4、业务级校验(最容易被低估)

业务级校验 ≠ 表单校验,它是 "提交前的最后防线"

特点:

  • 强依赖接口
  • 有副作用
  • 可能返回复杂错误结构
typescript 复制代码
async function businessValidate(values) {
  const res = await checkQuota(values.amount)
  if (!res.ok) {
    throw {
      fieldErrors: {
        amount: '额度不足'
      },
      formError: '当前账户无法提交'
    }
  }
}

正确处理方式:

typescript 复制代码
业务校验失败
 ├── 映射到字段
 ├── 映射到表单提示
 └── 阻断提交

5、校验统一调度流程(推荐)

typescript 复制代码
onSubmit
  ↓
字段级校验(全量)
  ↓
表单级校验
  ↓
业务级校验
  ↓
真正提交

不要让校验散落在各处

三、超复杂表单的性能优化(重灾区)

表单复杂度 ≠ 字段数量,而是:状态联动 × 校验规则 × 业务约束

"表单卡"本质不是表单,是 状态驱动 UI 的渲染失控

1、性能问题的三大元凶

❌ 常见错误

  • formValues 全量放在一个 state
  • 任意字段变化导致全表单 rerender
  • watch / effect 监听过多

2、表单性能优化的核心原则

  • 原则一:字段状态隔离
  • 原则二:最小更新粒度

(1)、字段状态隔离

typescript 复制代码
Field A state ≠ Field B state

每个 Field 自己订阅自己需要的数据

  • React Hook Form、Formily 的核心思想

(2)、最小更新粒度

typescript 复制代码
useField(name) // 只订阅 name 对应的值

而不是:

typescript 复制代码
useFormContext() // 订阅整个表单

3、表单项渲染优化策略

(1)、字段级 memo

typescript 复制代码
const Field = React.memo(({ schema }) => {
  ...
})

(2)、条件渲染惰性化

typescript 复制代码
if (!visible) return null

不要隐藏,用卸载。

4、超大表单(100+ 字段)的杀手锏

(1)、表单分区(Section)

表单分区可以分为:

  • 基础信息
  • 扩展信息
  • 风控信息

表单分区的特点:

  • 分模块挂载
  • 非激活区不渲染

(2)、虚拟化表单(极端场景)

只渲染可视区域字段

适用于:

  • 审批表单
  • 问卷
  • 长流程配置页

5、联动计算的性能控制

❌ 错误:

typescript 复制代码
watch(() => values, recalcAll)

✅ 正确:

typescript 复制代码
watch(['a', 'b'], recalcC)

建立 依赖图 + 精确触发

6、表单性能调优 Checklist

  • 字段是否独立订阅
  • 是否避免 formValues 全量传递
  • 是否使用 memo / useCallback
  • 是否拆分模块
  • 是否有联动依赖图
  • 是否卸载不可见字段

四、一个"成熟表单系统" + 企业级典例

1、一个"成熟表单系统"的终极形态

typescript 复制代码
Schema(配置数据)
 ├── 字段定义
 ├── 联动规则
 ├── 校验规则
 └── 权限 / 显隐

Form Engine(表单引擎)
 ├── 状态管理
 ├── 校验调度
 ├── 依赖追踪
 └── 性能优化

Renderer(渲染引擎)
 ├── FieldRenderer
 ├── LayoutRenderer
 └── CustomRenderer

这已经不是"写表单",而是在 "做表单平台"。

2、一个企业级典例

场景:企业「费用报销申请表单」

特点:

  • 字段多(40+)
  • 强联动
  • 强校验
  • 权限控制
  • 高性能要求

(1)、整体架构(严格对应你给的"终极形态")

typescript 复制代码
src/
├── schema/
│   ├── expense.schema.ts      # 字段 + 联动 + 校验 + 权限
│
├── engine/
│   ├── formEngine.ts          # 表单引擎(核心)
│   ├── validatorEngine.ts     # 校验调度
│   └── reactionEngine.ts      # 联动依赖处理
│
├── renderer/
│   ├── FormRenderer.tsx       # 表单渲染入口
│   ├── FieldRenderer.tsx      # 单字段渲染
│   └── LayoutRenderer.tsx     # 分区 / 布局
│
├── components/
│   ├── AmountInput.tsx        # 业务自定义组件
│
└── pages/
    └── ExpenseFormPage.tsx

(2)、Schema 层(灵魂)

①、字段 Schema 定义
typescript 复制代码
// schema/expense.schema.ts
export type FieldType = 'input' | 'select' | 'amount' | 'date'

export interface FieldSchema {
  name: string
  label: string
  type: FieldType

  required?: boolean
  rules?: any[]

  visible?: (values: any) => boolean
  disabled?: (values: any) => boolean

  options?: { label: string; value: any }[]

  permission?: string
}
②、企业级字段配置示例
typescript 复制代码
export const expenseSchema: FieldSchema[] = [
  {
    name: 'expenseType',
    label: '报销类型',
    type: 'select',
    required: true,
    options: [
      { label: '差旅', value: 'travel' },
      { label: '办公', value: 'office' }
    ]
  },
  {
    name: 'amount',
    label: '报销金额',
    type: 'amount',
    required: true,
    visible: (values) => !!values.expenseType,
    rules: [
      {
        validator: (_, value) => {
          if (value > 10000) {
            return Promise.reject('金额不能超过 1 万')
          }
          return Promise.resolve()
        }
      }
    ]
  },
  {
    name: 'travelReason',
    label: '出差原因',
    type: 'input',
    visible: (values) => values.expenseType === 'travel'
  }
]

⚠️ 注意:

  • Schema 里 没有 JSX
  • 没有 Form API
  • 只有"业务规则声明"

(3)、Renderer 层(UI 解耦)

①、组件注册中心(企业级必备)
typescript 复制代码
// renderer/componentRegistry.ts
import { Input, Select, DatePicker } from 'antd'
import AmountInput from '@/components/AmountInput'

export const componentRegistry = {
  input: Input,
  select: Select,
  date: DatePicker,
  amount: AmountInput
}
②、FieldRenderer(核心)
typescript 复制代码
// renderer/FieldRenderer.tsx
import { Form } from 'antd'
import { componentRegistry } from './componentRegistry'

export const FieldRenderer = ({ field, form }) => {
  const values = form.getFieldsValue()
  const visible = field.visible ? field.visible(values) : true

  if (!visible) return null

  const Comp = componentRegistry[field.type]

  return (
    <Form.Item
      name={field.name}
      label={field.label}
      rules={[
        ...(field.required ? [{ required: true }] : []),
        ...(field.rules || [])
      ]}
    >
      <Comp options={field.options} />
    </Form.Item>
  )
}

这里是关键点:

  • FieldRenderer 不知道业务

只负责:

  • visible
  • rules
  • component 映射
③、FormRenderer(表单入口)
typescript 复制代码
// renderer/FormRenderer.tsx
import { Form, Button } from 'antd'
import { FieldRenderer } from './FieldRenderer'

export const FormRenderer = ({ schema, onSubmit }) => {
  const [form] = Form.useForm()

  return (
    <Form
      form={form}
      layout="vertical"
      onFinish={onSubmit}
    >
      {schema.map((field) => (
        <FieldRenderer
          key={field.name}
          field={field}
          form={form}
        />
      ))}

      <Button type="primary" htmlType="submit">
        提交
      </Button>
    </Form>
  )
}

(4)、Engine 层(企业级真正价值)

①、校验调度引擎(业务级)
typescript 复制代码
// engine/validatorEngine.ts
export async function businessValidate(values) {
  if (values.amount > 5000 && !values.managerApproved) {
    throw {
      fieldErrors: {
        managerApproved: '超过 5000 需要主管审批'
      }
    }
  }
}
②、提交总控(非常重要)
typescript 复制代码
// engine/formEngine.ts
export async function submitForm(values) {
  await businessValidate(values)
  await api.submitExpense(values)
}

(5)、页面层(极薄)

typescript 复制代码
// pages/ExpenseFormPage.tsx
import { FormRenderer } from '@/renderer/FormRenderer'
import { expenseSchema } from '@/schema/expense.schema'
import { submitForm } from '@/engine/formEngine'

const ExpenseFormPage = () => {
  return (
    <FormRenderer
      schema={expenseSchema}
      onSubmit={submitForm}
    />
  )
}

export default ExpenseFormPage

(6)、这套设计为什么是"企业级"

对比普通写法:

维度 普通 Form 本方案
表单定义 JSX Schema
业务规则 分散在组件 集中
动态联动 useEffect 声明式
校验 rules 三层校验
扩展性 极强
性能 易全量渲染 可控
相关推荐
hhzz9 个月前
可视化动态表单动态表单界的天花板--Formily(阿里开源)
开源·可视化·动态表单
_xaboy10 个月前
FcDesigner页面样式错乱/功能不正常解决办法
vue.js·低代码·开源·动态表单·表单设计器
_xaboy1 年前
开源表单设计器form-create-designer如何保存设计器的规则和回显
vue.js·低代码·开源·动态表单·formcreate·低代码表单
_xaboy1 年前
开源动态表单form-create-designer 扩展个性化配置的最佳实践教程
vue.js·低代码·开源·动态表单·表单·formcreate·低代码表单
_xaboy1 年前
利用开源的低代码表单设计器FcDesigner高效管理和渲染复杂表单结构
vue.js·低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy1 年前
开源项目低代码表单设计器FcDesigner扩展自定义的容器组件.例如col
vue.js·低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy1 年前
开源项目低代码表单设计器FcDesigner扩展自定义组件
vue.js·低代码·开源·动态表单·formcreate·可视化表单设计器
_xaboy1 年前
开源项目低代码表单设计器FcDesigner扩展组件分组
低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy1 年前
使用开源的 Vue 移动端表单设计器创建表单
vue.js·低代码·开源·动态表单·formcreate·低代码表单