目录
- 前言
- 一、动态表单的「可配置化」设计
-
- 1、动态表单的本质
- [2、表单 Schema 的核心设计](#2、表单 Schema 的核心设计)
-
- [(1)、最小完备 Schema 结构](#(1)、最小完备 Schema 结构)
- (2)、组件注册机制(核心)
- [3、表单联动 & 条件渲染(最难点之一)](#3、表单联动 & 条件渲染(最难点之一))
- 4、动态表单的工程分层(很关键)
- [二、校验体系设计(字段级 / 表单级 / 业务级)](#二、校验体系设计(字段级 / 表单级 / 业务级))
-
- 1、三层校验的职责边界(非常重要)
- [2、字段级校验(Field Validation)](#2、字段级校验(Field Validation))
- [3、表单级校验(Form Validation)](#3、表单级校验(Form Validation))
- 4、业务级校验(最容易被低估)
- 5、校验统一调度流程(推荐)
- 三、超复杂表单的性能优化(重灾区)
-
- 1、性能问题的三大元凶
- 2、表单性能优化的核心原则
- 3、表单项渲染优化策略
-
- [(1)、字段级 memo](#(1)、字段级 memo)
- (2)、条件渲染惰性化
- [4、超大表单(100+ 字段)的杀手锏](#4、超大表单(100+ 字段)的杀手锏)
- 5、联动计算的性能控制
- [6、表单性能调优 Checklist](#6、表单性能调优 Checklist)
- [四、一个"成熟表单系统" + 企业级典例](#四、一个“成熟表单系统” + 企业级典例)
-
- 1、一个"成熟表单系统"的终极形态
- 2、一个企业级典例
-
- (1)、整体架构(严格对应你给的"终极形态")
- [(2)、Schema 层(灵魂)](#(2)、Schema 层(灵魂))
-
- [①、字段 Schema 定义](#①、字段 Schema 定义)
- ②、企业级字段配置示例
- [(3)、Renderer 层(UI 解耦)](#(3)、Renderer 层(UI 解耦))
- [(4)、Engine 层(企业级真正价值)](#(4)、Engine 层(企业级真正价值))
- (5)、页面层(极薄)
- (6)、这套设计为什么是"企业级"
前言
本文所涉及的表单以 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 | 三层校验 |
| 扩展性 | 差 | 极强 |
| 性能 | 易全量渲染 | 可控 |