基础表单设计:动态组件 vs 模板写法区别与优缺点分析
概述
在 Vue 3 + Element Plus 项目中构建表单有两种核心方案:模板写法(直接在 template 中编写表单结构)和 动态组件写法(通过 JSON Schema 配置驱动渲染)。本节对比两种方案的设计思路、实现方式和适用场景,帮助在不同业务场景下做出合理的技术选型。
方案对比总览
| 维度 | 模板写法 | 动态组件写法(Schema 驱动) |
|---|---|---|
| 学习成本 | 低 | 中高 |
| 灵活度 | 高(完全自定义) | 中(受 Schema 约束) |
| 可维护性 | 低(表单多时代码冗长) | 高(配置即代码) |
| 复用性 | 低 | 高(Schema 可复用/组合) |
| 类型安全 | 需手动维护 | 自动生成 |
| 适用场景 | 简单/定制化表单 | 大量相似表单/后台系统 |
模板写法实现
基础表单组件
<!-- components/BasicForm.vue -->
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
interface FormData {
name: string
email: string
phone: string
region: string
delivery: boolean
}
const formRef = ref<FormInstance>()
const formData = reactive<FormData>({
name: '',
email: '',
phone: '',
region: '',
delivery: false
})
const rules = reactive<FormRules<FormData>>({
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择区域', trigger: 'change' }
]
})
async function handleSubmit(): Promise<void> {
const valid = await formRef.value?.validate().catch(() => false)
if (valid) {
console.log('提交数据:', formData)
}
}
function handleReset(): void {
formRef.value?.resetFields()
}
</script>
<template>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="80px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" />
</el-form-item>
<el-form-item label="区域" prop="region">
<el-select v-model="formData.region" placeholder="请选择">
<el-option label="华东" value="east" />
<el-option label="华南" value="south" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
vue
动态组件写法实现(Schema 驱动)
Props 类型定义
// types/form.ts
import type { FormProps, FormItemProps } from 'element-plus'
/** 表单项 Schema 定义 */
export interface FormItemSchema {
/** 字段名(对应 model 中的 key) */
field: string
/** 标签文本 */
label: string
/** 组件类型 */
type: 'input' | 'select' | 'textarea' | 'switch' | 'date-picker' | 'radio'
/** 组件 props */
componentProps?: Record<string, unknown>
/** 校验规则 */
rules?: FormItemProps['rules']
/** 是否隐藏 */
hidden?: boolean
/** 栅格占位 */
span?: number
}
/** 表单 Schema */
export interface FormSchema {
/** 表单项配置列表 */
items: FormItemSchema[]
/** 表单 props(继承 ElForm) */
formProps?: Partial<FormProps>
}
typescript
Schema 驱动的表单组件
<!-- components/DynamicForm.vue -->
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
import type { FormInstance } from 'element-plus'
import type { FormSchema, FormItemSchema } from './types'
const props = defineProps<{
schema: FormSchema
model: Record<string, unknown>
}>()
const emit = defineEmits<{
submit: [values: Record<string, unknown>]
}>()
const formRef = ref<FormInstance>()
/** 组件类型映射 */
const componentMap: Record<FormItemSchema['type'], string> = {
'input': 'el-input',
'select': 'el-select',
'textarea': 'el-input',
'switch': 'el-switch',
'date-picker': 'el-date-picker',
'radio': 'el-radio-group'
}
/** 可见的表单项 */
const visibleItems = computed(() =>
props.schema.items.filter(item => !item.hidden)
)
async function handleSubmit(): Promise<void> {
const valid = await formRef.value?.validate().catch(() => false)
if (valid) {
emit('submit', { ...props.model })
}
}
function handleReset(): void {
formRef.value?.resetFields()
}
defineExpose({ validate: () => formRef.value?.validate() })
</script>
<template>
<el-form
ref="formRef"
:model="model"
v-bind="schema.formProps"
>
<el-row :gutter="20">
<el-col
v-for="item in visibleItems"
:key="item.field"
:span="item.span ?? 24"
>
<el-form-item :label="item.label" :prop="item.field" :rules="item.rules">
<component
:is="componentMap[item.type]"
v-model="model[item.field]"
v-bind="item.componentProps"
:type="item.type === 'textarea' ? 'textarea' : undefined"
>
<!-- select 的 options 插槽 -->
<template v-if="item.type === 'select'">
<el-option
v-for="opt in (item.componentProps?.options as any[])"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</template>
</component>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
vue
使用 Schema 表单
<!-- pages/UserForm.vue -->
<script setup lang="ts">
import { reactive } from 'vue'
import DynamicForm from '@/components/DynamicForm.vue'
import type { FormSchema } from '@/components/types'
const model = reactive({
name: '',
email: '',
role: '',
status: true
})
const schema: FormSchema = {
formProps: {
labelWidth: '80px',
labelPosition: 'right'
},
items: [
{
field: 'name',
label: '姓名',
type: 'input',
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
span: 12
},
{
field: 'email',
label: '邮箱',
type: 'input',
rules: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '格式不正确', trigger: 'blur' }
],
span: 12
},
{
field: 'role',
label: '角色',
type: 'select',
componentProps: {
options: [
{ label: '管理员', value: 'admin' },
{ label: '编辑', value: 'editor' },
{ label: '访客', value: 'viewer' }
]
},
span: 12
},
{
field: 'status',
label: '启用',
type: 'switch',
span: 12
}
]
}
function handleSubmit(values: Record<string, unknown>) {
console.log('提交:', values)
}
</script>
<template>
<DynamicForm :schema="schema" :model="model" @submit="handleSubmit" />
</template>
vue
两种方案的深入对比
模板写法优势
- 完全自定义布局、样式和交互逻辑
- IDE 模板语法高亮和智能提示完善
- 适合复杂表单(跨字段联动、条件渲染、自定义组件)
Schema 写法优势
- 表单配置与渲染逻辑分离,配置可序列化(存储/传输)
- 统一的数据结构,便于批量生成和管理
- 天然支持响应式布局(
span属性控制栅格)
选型建议
| 业务场景 | 推荐方案 |
|---|---|
| 1-3 个简单表单 | 模板写法 |
| 10+ 相似表单的后台系统 | Schema 写法 |
| 需要后端返回表单配置 | Schema 写法 |
| 高度定制化的复杂表单 | 模板写法 |
| 低代码/表单生成器 | Schema 写法 |
实践要点
- Props 类型通过
extends FormProps继承 Element Plus 的基础类型,再扩展自定义字段 - 动态组件使用 Vue 的
<component :is>语法,通过组件类型映射表选择渲染组件 - Schema 中的
span属性配合el-row/el-col实现响应式栅格布局 - 表单校验规则直接复用 Element Plus 的
FormRules类型,保持一致性 - 复杂表单场景(跨字段联动、异步校验)建议使用模板写法,避免 Schema 过于复杂
↑