表单验证 form-validation

使用 React Hook Form, Formik, Vee-Validate 和自定义验证器实现表单验证,包括客户端验证、服务器端同步和实时错误反馈。

前端开发 0 次安装 1 次浏览 更新于 3/3/2026

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)

资源