Vue 3 自定义表单系统实现指南

389 阅读3分钟

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);
    }
  }
}

八、性能优化建议

  1. 避免不必要的验证

    • 使用 trigger 控制验证时机
    • 实现验证防抖
    • 缓存验证结果
  2. 减少重渲染

    • 合理使用 v-memo
    • 优化响应式数据结构
    • 组件拆分合理
  3. 按需加载

    • 验证规则动态导入
    • 复杂验证逻辑懒加载

总结

这个表单系统实现了以下核心功能:

  1. 完整的类型支持
  2. 灵活的验证规则
  3. 可扩展的表单控件
  4. 统一的表单状态管理
  5. 友好的错误提示

建议根据实际项目需求进行适当的调整和扩展。

参考资源