动态表单设计解析

目录

前言

本文所涉及的表单以 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 三层校验
扩展性 极强
性能 易全量渲染 可控
相关推荐
Jeking2176 天前
低代码平台 表单设计器 unione form editor 功能组件 —— 悬浮按钮组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking2178 天前
实战案例|快捷键盘组件在仓库 PDA 入库表单中的落地应用
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking2178 天前
低代码平台 unione form editor 功能组件 —— 快捷键盘组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21712 天前
低代码平台 表单设计器 unione form editor 功能组件 —— 按钮组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21712 天前
低代码平台 表单设计器 unione form editor 功能组件 —— 进度条组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21715 天前
低代码平台表单设计器 unione form editor 组件 —— 子表单组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21715 天前
低代码平台表单设计器 unione-form-editor 组件 —— 子数据组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21716 天前
实战案例|引用组件在【销售订单表单】中的真实应用
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21716 天前
低代码平台表单设计器 unione-form-editor 组件 —— 条形码组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking21717 天前
低代码平台表单设计器 unione form editor 组件 —— 引用组件
低代码·动态表单·表单设计·表单引擎·unione cloud