Vue3 表单处理与验证
大约 9 分钟约 2819 字
Vue3 表单处理与验证
简介
表单是 Web 应用最常见的交互方式。Vue3 通过 v-model 双向绑定、computed 计算属性和 Composables 封装,可以高效处理表单数据。配合验证库(如 VeeValidate 或自定义验证),实现实时校验、错误提示和提交控制。
特点
基础表单
响应式表单
<!-- UserForm.vue -->
<script setup lang="ts">
import { reactive, ref } from 'vue'
interface UserForm {
name: string
email: string
phone: string
role: string
department: string
remark: string
notifications: boolean
}
const form = reactive<UserForm>({
name: '',
email: '',
phone: '',
role: '',
department: '',
remark: '',
notifications: true
})
// 保存初始值用于重置
const initialForm = { ...form }
// 验证规则
const errors = reactive<Record<string, string>>({})
const rules: Record<string, Rule[]> = {
name: [
{ required: true, message: '姓名不能为空' },
{ minLength: 2, message: '姓名至少2个字符' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{ pattern: /^[\w.-]+@[\w.-]+\.\w{2,}$/, message: '邮箱格式不正确' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
],
role: [{ required: true, message: '请选择角色' }]
}
// 验证单个字段
function validateField(field: string) {
errors[field] = ''
const fieldRules = rules[field] || []
const value = form[field as keyof UserForm]
for (const rule of fieldRules) {
if (rule.required && !value) {
errors[field] = rule.message
return false
}
if (rule.minLength && String(value).length < rule.minLength) {
errors[field] = rule.message
return false
}
if (rule.pattern && !rule.pattern.test(String(value))) {
errors[field] = rule.message
return false
}
}
return true
}
// 验证全部
function validateAll(): boolean {
let valid = true
for (const field of Object.keys(rules)) {
if (!validateField(field)) valid = false
}
return valid
}
// 提交
const submitting = ref(false)
async function handleSubmit() {
if (!validateAll()) return
submitting.value = true
try {
await api.createUser(form)
ElMessage.success('创建成功')
} catch (err) {
ElMessage.error('创建失败')
} finally {
submitting.value = false
}
}
// 重置
function resetForm() {
Object.assign(form, initialForm)
Object.keys(errors).forEach(k => errors[k] = '')
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<div class="form-item" :class="{ error: errors.name }">
<label>姓名 <span class="required">*</span></label>
<input v-model="form.name" @blur="validateField('name')" placeholder="请输入姓名" />
<span class="error-text">{{ errors.name }}</span>
</div>
<div class="form-item" :class="{ error: errors.email }">
<label>邮箱 <span class="required">*</span></label>
<input v-model="form.email" @blur="validateField('email')" type="email" />
<span class="error-text">{{ errors.email }}</span>
</div>
<div class="form-item">
<label>角色 <span class="required">*</span></label>
<select v-model="form.role" @change="validateField('role')">
<option value="">请选择</option>
<option value="admin">管理员</option>
<option value="editor">编辑</option>
</select>
<span class="error-text">{{ errors.role }}</span>
</div>
<div class="form-item">
<label>接收通知</label>
<input type="checkbox" v-model="form.notifications" />
</div>
<div class="form-actions">
<button type="submit" :disabled="submitting">
{{ submitting ? '提交中...' : '提交' }}
</button>
<button type="button" @click="resetForm">重置</button>
</div>
</form>
</template>表单 Composable
useForm 封装
// composables/useForm.ts
import { reactive, ref } from 'vue'
export function useForm<T extends Record<string, any>>(options: {
initialValues: T
rules?: Record<string, Rule[]>
onSubmit: (values: T) => Promise<void>
}) {
const form = reactive({ ...options.initialValues }) as T
const errors = reactive<Record<string, string>>({})
const submitting = ref(false)
const touched = reactive<Record<string, boolean>>({})
function validateField(field: string): boolean {
errors[field] = ''
const fieldRules = options.rules?.[field] || []
const value = form[field]
for (const rule of fieldRules) {
if (rule.required && !value) { errors[field] = rule.message; return false }
if (rule.minLength && String(value).length < rule.minLength) { errors[field] = rule.message; return false }
if (rule.pattern && value && !rule.pattern.test(String(value))) { errors[field] = rule.message; return false }
}
return true
}
function validateAll(): boolean {
let valid = true
for (const field of Object.keys(options.rules || {})) {
if (!validateField(field)) valid = false
}
return valid
}
async function handleSubmit() {
if (!validateAll()) return
submitting.value = true
try { await options.onSubmit({ ...form }) }
finally { submitting.value = false }
}
function resetForm() {
Object.assign(form, options.initialValues)
Object.keys(errors).forEach(k => errors[k] = '')
Object.keys(touched).forEach(k => touched[k] = false)
}
function handleBlur(field: string) {
touched[field] = true
validateField(field)
}
const isValid = computed(() => Object.values(errors).every(e => !e))
return { form, errors, submitting, touched, isValid, handleSubmit, resetForm, handleBlur, validateField, validateAll }
}
interface Rule {
required?: boolean
minLength?: number
maxLength?: number
pattern?: RegExp
message: string
}动态表单
跨字段验证与异步验证
// composables/useForm.ts — 扩展验证规则
interface Rule {
required?: boolean
minLength?: number
maxLength?: number
pattern?: RegExp
message: string
// 自定义验证函数
validator?: (value: any, form: Record<string, any>) => boolean | string | Promise<boolean | string>
// 异步验证(如检查用户名是否已存在)
asyncValidator?: (value: any) => Promise<boolean | string>
}
// 跨字段验证示例:确认密码
const registerRules: Record<string, Rule[]> = {
password: [
{ required: true, message: '密码不能为空' },
{ minLength: 8, message: '密码至少8位' },
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, message: '需包含大小写字母和数字' }
],
confirmPassword: [
{ required: true, message: '请确认密码' },
{
validator: (value, form) => {
if (value !== form.password) return '两次密码不一致'
return true
}
}
],
}
// 异步验证示例:检查用户名唯一性
const usernameRules: Rule[] = [
{ required: true, message: '用户名不能为空' },
{ minLength: 3, message: '用户名至少3个字符' },
{
asyncValidator: async (value) => {
const response = await fetch(`/api/check-username?username=${value}`)
const { exists } = await response.json()
if (exists) return '用户名已被占用'
return true
}
}
]
// 扩展 validateField 支持异步验证
async function validateField(field: string): Promise<boolean> {
errors[field] = ''
const fieldRules = rules[field] || []
const value = form[field]
for (const rule of fieldRules) {
// 同步验证
if (rule.required && !value) {
errors[field] = rule.message
return false
}
if (rule.minLength && String(value).length < rule.minLength) {
errors[field] = rule.message
return false
}
if (rule.pattern && value && !rule.pattern.test(String(value))) {
errors[field] = rule.message
return false
}
// 自定义同步验证
if (rule.validator) {
const result = rule.validator(value, form)
if (result !== true) {
errors[field] = typeof result === 'string' ? result : rule.message
return false
}
}
// 异步验证
if (rule.asyncValidator) {
const asyncResult = await rule.asyncValidator(value)
if (asyncResult !== true) {
errors[field] = typeof asyncResult === 'string' ? asyncResult : rule.message
return false
}
}
}
return true
}VeeValidate 集成
// 使用 VeeValidate + Zod 进行表单验证
// npm install vee-validate @vee-validate/zod zod
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
// 定义验证 Schema
const userSchema = z.object({
name: z.string().min(2, '姓名至少2个字符').max(20, '姓名最多20个字符'),
email: z.string().email('请输入有效邮箱'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效手机号').optional().or(z.literal('')),
role: z.enum(['admin', 'editor', 'viewer'], { message: '请选择角色' }),
department: z.string().min(1, '请选择部门'),
age: z.number().min(18, '年龄不能小于18').max(65, '年龄不能大于65').optional(),
website: z.string().url('请输入有效 URL').optional().or(z.literal('')),
})
type UserFormValues = z.infer<typeof userSchema>
// 在组件中使用
const { handleSubmit, errors, defineField, isSubmitting, resetForm, setFieldValue } = useForm<UserFormValues>({
validationSchema: toTypedSchema(userSchema),
initialValues: {
name: '',
email: '',
phone: '',
role: 'viewer',
department: '',
},
})
const [name, nameAttrs] = defineField('name')
const [email, emailAttrs] = defineField('email')
const [phone, phoneAttrs] = defineField('phone')
const [role, roleAttrs] = defineField('role')
const onSubmit = handleSubmit(async (values) => {
await api.createUser(values)
ElMessage.success('创建成功')
})<!-- 使用 VeeValidate 的表单模板 -->
<template>
<form @submit="onSubmit">
<div>
<label>姓名</label>
<input v-model="name" v-bind="nameAttrs" />
<span class="error">{{ errors.name }}</span>
</div>
<div>
<label>邮箱</label>
<input v-model="email" v-bind="emailAttrs" type="email" />
<span class="error">{{ errors.email }}</span>
</div>
<div>
<label>角色</label>
<select v-model="role" v-bind="roleAttrs">
<option value="admin">管理员</option>
<option value="editor">编辑</option>
<option value="viewer">查看者</option>
</select>
<span class="error">{{ errors.role }}</span>
</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</form>
</template>表单步骤向导
<!-- FormWizard.vue — 多步骤表单 -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const currentStep = ref(0)
const totalSteps = 3
const step1Data = reactive({ name: '', email: '' })
const step2Data = reactive({ company: '', department: '', position: '' })
const step3Data = reactive({ receiveNewsletter: true, preferences: [] as string[] })
const allData = computed(() => ({
...step1Data,
...step2Data,
...step3Data,
}))
function isStepValid(step: number): boolean {
switch (step) {
case 0: return !!(step1Data.name && step1Data.email)
case 1: return !!(step2Data.company && step2Data.department)
case 2: return true
default: return false
}
}
function nextStep() {
if (isStepValid(currentStep.value)) {
currentStep.value++
}
}
function prevStep() {
if (currentStep.value > 0) currentStep.value--
}
async function submitAll() {
await api.submitRegistration(allData.value)
}
</script>
<template>
<div class="form-wizard">
<!-- 步骤指示器 -->
<div class="steps-indicator">
<div v-for="step in totalSteps" :key="step"
:class="['step', { active: currentStep === step - 1, completed: currentStep > step - 1 }]">
<span class="step-number">{{ step }}</span>
<span class="step-label">
{{ step === 1 ? '基本信息' : step === 2 ? '工作信息' : '偏好设置' }}
</span>
</div>
</div>
<!-- 步骤内容 -->
<div class="step-content">
<!-- Step 1 -->
<div v-show="currentStep === 0">
<input v-model="step1Data.name" placeholder="姓名" />
<input v-model="step1Data.email" placeholder="邮箱" type="email" />
</div>
<!-- Step 2 -->
<div v-show="currentStep === 1">
<input v-model="step2Data.company" placeholder="公司" />
<input v-model="step2Data.department" placeholder="部门" />
<input v-model="step2Data.position" placeholder="职位" />
</div>
<!-- Step 3 -->
<div v-show="currentStep === 2">
<label>
<input type="checkbox" v-model="step3Data.receiveNewsletter" />
接收邮件通知
</label>
</div>
</div>
<!-- 导航按钮 -->
<div class="wizard-actions">
<button v-if="currentStep > 0" @click="prevStep">上一步</button>
<button v-if="currentStep < totalSteps - 1" @click="nextStep">下一步</button>
<button v-else @click="submitAll">提交</button>
</div>
</div>
</template>根据配置生成
<!-- DynamicForm.vue -->
<script setup lang="ts">
interface FormField {
key: string
label: string
type: 'text' | 'email' | 'number' | 'select' | 'textarea' | 'checkbox'
placeholder?: string
options?: { label: string; value: string }[]
required?: boolean
}
const props = defineProps<{
fields: FormField[]
modelValue: Record<string, any>
}>()
const emit = defineEmits<{ 'update:modelValue': [value: Record<string, any>] }>()
</script>
<template>
<form>
<div v-for="field in fields" :key="field.key" class="form-item">
<label>{{ field.label }}</label>
<input v-if="field.type === 'text' || field.type === 'email'"
:type="field.type"
:value="modelValue[field.key]"
@input="emit('update:modelValue', { ...modelValue, [field.key]: $event.target.value })" />
<select v-else-if="field.type === 'select'"
:value="modelValue[field.key]"
@change="emit('update:modelValue', { ...modelValue, [field.key]: $event.target.value })">
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
<textarea v-else-if="field.type === 'textarea'"
:value="modelValue[field.key]"
@input="emit('update:modelValue', { ...modelValue, [field.key]: $event.target.value })" />
</div>
</form>
</template>优点
缺点
总结
Vue3 表单核心:v-model 双向绑定 + reactive 响应式 + 自定义验证。推荐用 Composable(useForm)封装验证逻辑。动态表单通过 JSON 配置生成,适合后台管理场景。验证时机推荐 blur 触发 + submit 时全量验证。Element Plus 的 el-form 适合快速开发,自定义表单更灵活。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Vue3 表单处理与验证》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Vue3 表单处理与验证》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Vue3 表单处理与验证》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Vue3 表单处理与验证》最大的收益和代价分别是什么?
