name: form-validation description: 实现使用 React Hook Form, Formik, Vee-Validate 和自定义验证器的表单验证。在构建具有实时验证的强大表单处理时使用。
表单验证
概览
实现包括客户端验证、服务器端同步和实时错误反馈在内的全面表单验证,并使用 TypeScript 类型安全性。
何时使用
- 用户输入验证
- 表单提交处理
- 实时错误反馈
- 复杂验证规则
- 多步表单
实现示例
1. React Hook Form 与 TypeScript
// types/form.ts
export interface LoginFormData {
email: string;
password: string;
rememberMe: boolean;
}
export interface RegisterFormData {
email: string;
password: string;
confirmPassword: string;
name: string;
terms: boolean;
}
// components/LoginForm.tsx
import { useForm, SubmitHandler } from 'react-hook-form';
import { LoginFormData } from '../types/form';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const LoginForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch
} = useForm<LoginFormData>({
defaultValues: {
email: '',
password: '',
rememberMe: false
}
});
const onSubmit: SubmitHandler<LoginFormData> = async (data) => {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('登录失败');
// 处理成功
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>邮箱</label>
<input
type="email"
{...register('email', {
required: '邮箱是必填项',
pattern: {
value: emailRegex,
message: '无效的邮箱格式'
}
})}
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<label>密码</label>
<input
type="password"
{...register('password', {
required: '密码是必填项',
minLength: {
value: 8,
message: '密码至少需要8个字符'
}
})}
/>
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<div>
<label>
<input type="checkbox" {...register('rememberMe')} />
记住我
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '正在登录...' : '登录'}
</button>
</form>
);
};
// 自定义验证器
const usePasswordStrength = () => {
return (password: string): boolean | string => {
if (password.length < 8) return '至少8个字符';
if (!/[A-Z]/.test(password)) return '至少一个大写字母';
if (!/[0-9]/.test(password)) return '至少一个数字';
return true;
};
};
2. Formik 与 Yup 验证
// validationSchema.ts
import * as Yup from 'yup';
export const registerValidationSchema = Yup.object().shape({
email: Yup.string()
.email('无效的邮箱')
.required('邮箱是必填项'),
password: Yup.string()
.min(8, '密码至少需要8个字符')
.matches(/[A-Z]/, '必须包含大写字母')
.matches(/[0-9]/, '必须包含数字')
.required('密码是必填项'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], '密码必须匹配')
.required('确认密码是必填项'),
name: Yup.string()
.min(2, '姓名太短')
.required('姓名是必填项'),
terms: Yup.boolean()
.oneOf([true], '您必须接受条款')
.required()
});
// components/RegisterForm.tsx
import { Formik, Form, Field, ErrorMessage } from 'formik';
import { registerValidationSchema } from '../validationSchema';
import { RegisterFormData } from '../types/form';
export const RegisterForm: React.FC = () => {
const initialValues: RegisterFormData = {
email: '',
password: '',
confirmPassword: '',
name: '',
terms: false
};
const handleSubmit = async (
values: RegisterFormData,
{ setSubmitting, setFieldError }: any
) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(values)
});
if (!response.ok) {
const error = await response.json();
if (error.emailExists) {
setFieldError('email', '邮箱已注册');
}
throw new Error(error.message);
}
} catch (error) {
console.error(error);
} finally {
setSubmitting(false);
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={registerValidationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, isValid }) => (
<Form>
<div>
<label htmlFor="name">姓名</label>
<Field name="name" type="text" />
<ErrorMessage name="name" component="span" className="error" />
</div>
<div>
<label htmlFor="email">邮箱</label>
<Field name="email" type="email" />
<ErrorMessage name="email" component="span" className="error" />
</div>
<div>
<label htmlFor="password">密码</label>
<Field name="password" type="password" />
<ErrorMessage name="password" component="span" className="error" />
</div>
<div>
<label htmlFor="confirmPassword">确认密码</label>
<Field name="confirmPassword" type="password" />
<ErrorMessage name="confirmPassword" component="span" className="error" />
</div>
<div>
<label>
<Field name="terms" type="checkbox" />
我同意条款
</label>
<ErrorMessage name="terms" component="span" className="error" />
</div>
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? '正在注册...' : '注册'}
</button>
</Form>
)}
</Formik>
);
};
3. Vue Vee-Validate
// validationRules.ts
import { defineRule } from 'vee-validate';
import { email, required, min, confirmed } from '@vee-validate/rules';
defineRule('required', required);
defineRule('email', email);
defineRule('min', min);
defineRule('confirmed', confirmed);
defineRule('password-strength', (value: string) => {
if (value.length < 8) return '密码至少需要8个字符';
if (!/[A-Z]/.test(value)) return '必须包含大写字母';
if (!/[0-9]/.test(value)) return '必须包含数字';
return true;
});
// components/LoginForm.vue
<template>
<Form @submit="onSubmit" :validation-schema="validationSchema">
<div class="form-group">
<label for="email">邮箱</label>
<Field name="email" type="email" as="input" class="form-control" />
<ErrorMessage name="email" class="error" />
</div>
<div class="form-group">
<label for="password">密码</label>
<Field name="password" type="password" as="input" class="form-control" />
<ErrorMessage name="password" class="error" />
</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '正在登录...' : '登录' }}
</button>
</Form>
</template>
<script setup lang="ts">
import { Form, Field, ErrorMessage } from 'vee-validate';
import { object, string } from 'yup';
import { ref } from 'vue';
const isSubmitting = ref(false);
const validationSchema = object({
email: string().email('无效的邮箱').required('邮箱是必填项'),
password: string().required('密码是必填项')
});
const onSubmit = async (values: any) => {
isSubmitting.value = true;
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(values)
});
if (!response.ok) throw new Error('登录失败');
} catch (error) {
console.error(error);
} finally {
isSubmitting.value = false;
}
};
</script>
4. 自定义验证器 Hook
// hooks/useFieldValidator.ts
import { useState, useCallback } from 'react';
export interface ValidationRule {
validate: (value: any) => boolean | string;
message: string;
}
export interface FieldError {
isValid: boolean;
message: string | null;
}
export const useFieldValidator = (rules: ValidationRule[] = []) => {
const [error, setError] = useState<FieldError>({
isValid: true,
message: null
});
const validate = useCallback((value: any) => {
for (const rule of rules) {
const result = rule.validate(value);
if (result !== true) {
setError({
isValid: false,
message: typeof result === 'string' ? result : rule.message
});
return false;
}
}
setError({
isValid: true,
message: null
});
return true;
}, [rules]);
const clearError = useCallback(() => {
setError({
isValid: true,
message: null
});
}, []);
return { error, validate, clearError };
};
// 使用
const { error: emailError, validate: validateEmail } = useFieldValidator([
{
validate: (v) => v.length > 0,
message: '邮箱是必填项'
},
{
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: '无效的邮箱格式'
}
]);
5. 服务器端验证集成
// 异步服务器验证
const useAsyncValidation = () => {
const validateEmail = async (email: string) => {
const response = await fetch(`/api/validate/email?email=${email}`);
const { available } = await response.json();
return available ? true : '邮箱已注册';
};
const validateUsername = async (username: string) => {
const response = await fetch(`/api/validate/username?username=${username}`);
const { available } = await response.json();
return available ? true : '用户名已被占用';
};
return { validateEmail, validateUsername };
};
// React Hook Form 与异步验证
const { validateEmail } = useAsyncValidation();
register('email', {
required: '邮箱是必填项',
validate: async (value) => {
return await validateEmail(value);
}
});
最佳实践
- 客户端和服务器端都进行验证
- 提供实时反馈
- 使用 TypeScript 进行类型安全
- 为复杂规则实现自定义验证器
- 正确处理异步验证
- 显示清晰的错误消息
- 在验证失败时保留用户输入
- 彻底测试验证规则
- 使用模式验证(Yup, Zod)