Vue 3 自定义表单系统实现指南
一、基础架构设计
目录结构
components/
└── Form/
├── index.ts
├── Form.vue
├── FormItem.vue
├── src/
│ ├── types.ts
│ ├── hooks.ts
│ ├── utils.ts
│ └── validator.ts
└── components/
├── Input.vue
├── Select.vue
└── Checkbox.vue
二、类型定义
// src/types.ts
export type FormRule = {
required?: boolean
message?: string
validator?: (value: any) => boolean | Promise<boolean>
trigger?: 'blur' | 'change'
min?: number
max?: number
pattern?: RegExp
}
export type FormRules = {
[key: string]: FormRule[]
}
export type FormInstance = {
validate: () => Promise<boolean>
resetFields: () => void
clearValidate: (props?: string[]) => void
getFieldValue: (prop: string) => any
setFieldValue: (prop: string, value: any) => void
}
export type FormItemContext = {
validate: () => Promise<boolean>
resetField: () => void
clearValidate: () => void
}
三、核心组件实现
1. Form 组件
<!-- Form.vue -->
<template>
<form class="custom-form" @submit.prevent>
<slot></slot>
</form>
</template>
<script setup lang="ts">
import { provide, reactive, ref } from 'vue'
import type { FormRules, FormInstance } from './src/types'
const props = defineProps<{
model: Record<string, any>
rules?: FormRules
}>()
// 存储所有 FormItem 实例
const formItems = ref(new Set<FormItemContext>())
// 注册 FormItem
const registerFormItem = (item: FormItemContext) => {
formItems.value.add(item)
return () => {
formItems.value.delete(item)
}
}
// 提供表单上下文
provide('form', {
model: props.model,
rules: props.rules,
registerFormItem
})
// 表单方法
const validate = async () => {
const promises = Array.from(formItems.value).map(item => item.validate())
const results = await Promise.all(promises)
return results.every(result => result)
}
const resetFields = () => {
formItems.value.forEach(item => item.resetField())
}
const clearValidate = () => {
formItems.value.forEach(item => item.clearValidate())
}
// 暴露表单实例方法
defineExpose<FormInstance>({
validate,
resetFields,
clearValidate,
getFieldValue: (prop: string) => props.model[prop],
setFieldValue: (prop: string, value: any) => {
props.model[prop] = value
}
})
</script>
2. FormItem 组件
<!-- FormItem.vue -->
<template>
<div class="form-item" :class="{ 'is-error': validateState === 'error' }">
<label v-if="label" :for="prop">{{ label }}</label>
<div class="form-item-content">
<slot></slot>
<div v-if="validateState === 'error'" class="form-item-error">
{{ validateMessage }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, inject, onMounted, onBeforeUnmount } from 'vue'
import { useFormItemValidate } from './src/hooks'
const props = defineProps<{
prop: string
label?: string
rules?: FormRule[]
}>()
const form = inject('form')
const { validateState, validateMessage, validate, resetField, clearValidate } =
useFormItemValidate(props.prop, props.rules)
// 注册到 Form
onMounted(() => {
if (form?.registerFormItem) {
const unregister = form.registerFormItem({
validate,
resetField,
clearValidate
})
onBeforeUnmount(unregister)
}
})
</script>
3. 验证 Hook
// src/hooks.ts
import { ref, computed, inject } from 'vue'
import { validateRules } from './validator'
export function useFormItemValidate(prop: string, rules?: FormRule[]) {
const form = inject('form')
const validateState = ref('')
const validateMessage = ref('')
const validate = async () => {
if (!form || !prop || !rules) return true
const value = form.model[prop]
const formRules = [...(rules || []), ...(form.rules?.[prop] || [])]
if (!formRules.length) return true
validateState.value = 'validating'
try {
await validateRules(value, formRules)
validateState.value = 'success'
validateMessage.value = ''
return true
} catch (error) {
validateState.value = 'error'
validateMessage.value = error.message
return false
}
}
const resetField = () => {
if (!form || !prop) return
form.model[prop] = initialValue
clearValidate()
}
const clearValidate = () => {
validateState.value = ''
validateMessage.value = ''
}
return {
validateState,
validateMessage,
validate,
resetField,
clearValidate
}
}
四、表单验证实现
// src/validator.ts
export async function validateRules(value: any, rules: FormRule[]) {
for (const rule of rules) {
const { required, message, validator, min, max, pattern } = rule
// 必填验证
if (required && !value) {
throw new Error(message || '该字段为必填项')
}
// 自定义验证
if (validator) {
const result = await validator(value)
if (!result) {
throw new Error(message || '验证失败')
}
}
// 长度验证
if (typeof value === 'string') {
if (min && value.length < min) {
throw new Error(message || `长度不能小于${min}`)
}
if (max && value.length > max) {
throw new Error(message || `长度不能大于${max}`)
}
}
// 正则验证
if (pattern && !pattern.test(value)) {
throw new Error(message || '格式不正确')
}
}
}
五、自定义表单控件
1. Input 组件
<!-- components/Input.vue -->
<template>
<div class="form-input">
<input
:value="modelValue"
@input="handleInput"
@blur="handleBlur"
v-bind="$attrs"
/>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'blur'): void
}>()
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
emit('update:modelValue', value)
}
const handleBlur = () => {
emit('blur')
}
</script>
2. Select 组件
<!-- components/Select.vue -->
<template>
<div class="form-select">
<select :value="modelValue" @change="handleChange">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: any
options: Array<{ label: string; value: any }>
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
}>()
const handleChange = (e: Event) => {
const value = (e.target as HTMLSelectElement).value
emit('update:modelValue', value)
}
</script>
六、使用示例
<template>
<Form :model="formData" :rules="rules" ref="formRef">
<FormItem prop="username" label="用户名">
<Input v-model="formData.username" />
</FormItem>
<FormItem prop="age" label="年龄">
<Input v-model="formData.age" type="number" />
</FormItem>
<FormItem prop="gender" label="性别">
<Select v-model="formData.gender" :options="genderOptions" />
</FormItem>
<button @click="handleSubmit">提交</button>
</Form>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Form, FormItem, Input, Select } from './components/Form'
const formRef = ref<FormInstance>()
const formData = reactive({
username: '',
age: '',
gender: ''
})
const rules = {
username: [
{ required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度在3-20个字符之间' }
],
age: [
{ required: true, message: '请输入年龄' },
{ validator: (val) => Number(val) >= 18, message: '年龄必须大于18岁' }
],
gender: [
{ required: true, message: '请选择性别' }
]
}
const genderOptions = [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
const handleSubmit = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (valid) {
console.log('表单验证通过', formData)
} else {
console.log('表单验证失败')
}
}
</script>
七、样式设计
.custom-form {
.form-item {
margin-bottom: 20px;
&.is-error {
.form-item-content {
input, select {
border-color: var(--error-color);
}
}
.form-item-error {
color: var(--error-color);
font-size: 12px;
margin-top: 4px;
}
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
}
input, select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
transition: all 0.3s;
&:focus {
outline: none;
border-color: var(--primary-color);
}
}
}
八、性能优化建议
-
避免不必要的验证
- 使用
trigger控制验证时机 - 实现验证防抖
- 缓存验证结果
- 使用
-
减少重渲染
- 合理使用
v-memo - 优化响应式数据结构
- 组件拆分合理
- 合理使用
-
按需加载
- 验证规则动态导入
- 复杂验证逻辑懒加载
总结
这个表单系统实现了以下核心功能:
- 完整的类型支持
- 灵活的验证规则
- 可扩展的表单控件
- 统一的表单状态管理
- 友好的错误提示
建议根据实际项目需求进行适当的调整和扩展。