VForm 基础封装与 Schema 渲染起步
概述
确定用 Schema 驱动表单后,下一步是把这套思路落地成真正能跑的组件。本章从 VForm 与 VFormItem 的初步拆分讲起,定义 FormSchema 的核心字段,并对比"模板 v-if 分发"与"纯 TSX 渲染"两条路线。首版我们优先选择模板方案,原因是它能复用 Vue 的自动导入与 Element Plus 的样式生态,开发更顺手;同时列出第一批要支持的控件类型(Input、Select、DatePicker、TimePicker),为后续动态组件收口打下基础。
学习目标
- 掌握
VForm继承FormProps的基础封装方式 - 理解
VFormItem作为字段级中间层的作用 - 定义
FormSchema的type/field/attrs/events核心字段 - 比较模板
v-if分发与纯 TSX 渲染两条路线 - 理解首版优先模板方案的现实考量
- 认识 Input / Select / DatePicker / TimePicker 作为高频首批控件
- 掌握动态组件里
v-model与modelValue的绑定写法 - 了解控件注册与动态解析的预留位置
- 掌握一个最小可运行的 VForm 雏形
一、VForm 直接继承 Element Plus 的 Form Props
最省事的封装方式是让 VForm 的 props 直接继承 FormProps(model、rules、label-width 等),这样官方表格所有常用属性天然透传,不用一个个手写。组件内部只额外加少量自研字段(如 schema),其余全部通过 v-bind="props" 落到 el-form 上。这一步和前面 VTable 继承 TableProps 是同一思路:先不重新发明轮子,把官方能力原样接住。
二、VFormItem 是字段级的中间层
VForm 负责整表容器,VFormItem 负责单个字段的渲染。它拿到一个 item: FormSchema,根据 item.type 决定渲染哪个控件,再把 item.attrs、item.events 透传给具体控件。把"整表"和"单字段"拆成两层,是为了让每个字段的渲染逻辑独立、可测试、可扩展;后续加校验、加插槽、加实例暴露,都是在 VFormItem 这一层叠加,不会污染整表容器。
三、FormSchema 的核心字段
一份字段配置至少包含:type(控件类型)、field(字段名,对应 model 的 key)、label(标签)、value(默认值)、attrs(透传给控件的属性)、events(透传给控件的事件)。其中 field 与 value 决定数据,attrs 与 events 决定控件行为,type 决定渲染分支。这个结构刻意保持小而稳,后续校验规则、子布局、插槽都是在此基础上加字段,而不是推翻重写。
export interface FormSchema {
type?: string
field?: string
label?: string
value?: any
attrs?: Record<string, unknown>
events?: Record<string, (...args: any[]) => void>
}四、模板 v-if 分发 vs 纯 TSX 渲染
渲染每个字段有两种典型写法。一种是模板里用 v-if / v-else-if 按 type 分支渲染不同控件;另一种是纯 TSX 里用 h() 表达分支或直接 createVNode。模板方案的好处是和 el-form-item、el-input 这些组件天然配合,自动导入和样式生态开箱即用;TSX 方案更灵活、表达式更自由,但首版要额外处理类型与 JSX 配置。考虑到后台组件库追求稳妥与可读性,首版优先模板分发。
五、attrs 与 events 的透传细节
item.attrs 通过 v-bind="item.attrs" 落到控件上,item.events 则需要在模板里逐一映射(如 @change="item.events?.change"),或用一个事件对象循环绑定。这里要注意事件名必须与官方组件保持一致(如 change、blur、focus),不要自创命名,否则页面层传进来的回调接不上。透传层越薄越好,把"控件有什么事件"交给官方文档,组件只做搬运。
六、动态组件里的 v-model 绑定
即便首版用模板分发,也要想清楚 v-model 怎么接。以 Input 为例:<el-input v-model="model[item.field]" v-bind="item.attrs" />。关键点在于 model 是 VForm 内部维护的对象,每个 VFormItem 只绑定自己那一份 model[item.field]。这样所有字段的改动都落在同一个 model 上,对外只需暴露这一个对象,避免每个控件各自持有独立状态导致数据分散。
七、VForm 最小可运行雏形
把上面几点拼成一个最小骨架,VForm 遍历 Schema 渲染 VFormItem,VFormItem 按 type 分发控件。这个雏形虽还没有校验和扁平化,但已经能"给一份配置就出一屏表单",足以验证渲染链路的成立。
<template>
<el-form v-bind="props">
<VFormItem
v-for="(item, index) in schema"
:key="index"
:item="item"
v-model="model[item.field]"
/>
</el-form>
</template>八、控件注册与动态解析的预留位置
即便首版走模板分支,也应在 VFormItem 里预留"根据 type 解析真正组件"的接入点——后续章节会用 component :is="type" 或一张 ComponentType 映射表替换 v-if 链。提前留好这个位置,能让模板方案平滑演进到动态组件方案,而不必推翻重来。这也是"先保守实现、再收敛简化"的具体落点。
九、第一批高频控件:Input / Select / DatePicker / TimePicker
首版不需要贪多,先把四个最高频的控件接进来:Input(文本输入)、Select(下拉选择)、DatePicker(日期)、TimePicker(时间)。它们覆盖了绝大多数录入场景,且各自代表了不同复杂度——Input 最单纯,Select 带选项子节点,Date/Time 带 value-format 与 Date 对象语义。把这四个做扎实,等于验证了整套渲染器的核心机制,后续加 Switch、Rate、Checkbox 等都只是照葫芦画瓢。
十、与 VTable 渲染对照
VTable 用 columns 配置驱动列,VForm 用 schema 配置驱动字段,两者都是"配置 → 递归渲染"的范式。区别是表格的子节点是列、表单的子节点是字段,且表单字段自带 label、校验和嵌套。理解这种对照,能在维护两套组件时复用同一套递归心智,而不是各写一套独立的遍历逻辑。
十一、首版优先模板方案的现实考量
选择模板不是因为它更高级,而是因为它当下的工程成本低:组件自动导入、scoped 样式、v-model 语法糖都不需要额外搭建。等控件种类和分支真正多到模板难以维护时,再考虑向动态组件或 TSX 收口也不迟。组件库开发的节奏普遍是先"保守实现跑通",再"理解边界后收敛简化",这一步正是该节奏的起点。
十二、封装第一性原理小结
把本章落地的几个决策收一下,后面无论怎么演进都不该违背:
- 继承而非重写:直接
extends FormProps,官方能力原样透传,不重新造轮子 - 分层而非混写:
VForm管整表、VFormItem管字段,职责边界清晰 - 配置而非硬编码:
type/field/value/attrs/events决定一切,模板只做分发 - 保守而非冒进:首版用模板分发、
v-model直绑,理解边界后再收口动态组件 - 透传而非自创:事件名与官方一致,组件只做搬运,不发明新命名
这五条其实是从"需求分析边界清单"推导下来的工程落地版。封装 Form 组件最忌讳的就是在不知边界的情况下一口气把所有控件和属性都塞进一个组件,导致后续任何改动都牵一发动全身。
封装的第一性原理说到底就是一句话:让高频路径简单、让边界清晰、让扩展有路。违背任何一条,组件都会随着业务增长而慢慢腐烂,而 Schema Form 这种本来就偏重的抽象,对这三条的敏感度更高。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| VForm 把官方属性一个个重写很累 | 没继承 FormProps | 用 extends FormProps 原样透传 |
| 单字段逻辑和整表逻辑混在一起 | 没拆 VFormItem 中间层 | 整表与字段分成两层组件 |
| 首版该用模板还是 TSX 纠结 | 没考虑自动导入与样式生态 | 首版优先模板,复杂度上来再收口 |
| 控件类型一多模板就爆炸 | 一开始就想覆盖全部控件 | 先接 Input/Select/Date/Time 四个高频 |
field 与 value 职责混淆 | Schema 字段定义不清 | 明确 field 管 key、value 管默认初值 |
| 页面层回调接不上控件事件 | 自创了事件名 | 事件名与官方组件保持一致 |