验证规则

Framework:

概述

Schema FormX 提供了灵活的验证机制,支持内置验证、自定义验证函数和异步验证。本章节将详细介绍验证规则的使用方法。

内置验证

必填验证

通过 required 属性设置字段为必填:

{
  type: 'string',
  name: 'username',
  label: '用户名',
  required: true  // 必填
}

按字段类型的空值判断规则

必填验证会根据字段类型判断空值,不同类型的空值判断规则如下:

字段类型空值条件验证失败提示
stringnull, undefined, ''${label}不能为空
numbernull, undefined, ''${label}不能为空
booleannull, undefined, ''${label}不能为空
objectnull, undefined, {}${label}不能为空
arraynull, undefined, []${label}不能为空

验证逻辑源码

// 必填验证的核心逻辑
if (
  required &&
  (value === null ||
    value === undefined ||
    value === '' ||
    (Array.isArray(value) && value.length === 0) ||
    isEqual(value, {}))
) {
  errors[name] = `\${label}不能为空`;
}

注意事项

  • boolean 类型的 false 不是空值,必填验证会通过
  • number 类型的 0 不是空值,必填验证会通过
  • 空字符串 '' 对所有类型都是空值
  • hidden: true 的字段仍会参与验证 —— 如果隐藏字段设置了 required: true,仍会触发验证错误。如需跳过隐藏字段的验证,请使用条件 required 或自定义 validator
  • 如果需要更复杂的必填判断,可以使用自定义验证器
  • 错误消息:必填验证的错误消息为中文硬编码的 ${label}不能为空,不可自定义。如果需要自定义错误消息(如英文提示),请使用自定义 validator 函数替代 required 属性。国际化支持列入 TODO

字段默认值

表单初始化时,会根据字段类型自动设置默认值:

字段类型默认值
string''
numbernull
booleannull
object{}
array[]

可以通过 defaultData 属性覆盖默认值:

<Form 
  schema={schema}
  defaultData={{
    name: '张三',      // 覆盖 string 默认值
    age: 25,            // 覆盖 number 默认值
    agree: true         // 覆盖 boolean 默认值
  }}
/>

自定义验证函数

基础用法

通过 validator 属性自定义验证逻辑:

{
  type: 'string',
  name: 'email',
  label: '邮箱',
  validator: ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return `\${label}格式不正确`;
    }
    return undefined;  // 验证通过
  }
}

验证函数参数

验证函数接收以下参数:

type ValidatorParams = {
  value: unknown;           // 当前字段值
  label: string;            // 字段显示名称
  data: Readonly<Record<string, unknown>>;  // 整个表单数据
};

type Validator = (params: ValidatorParams) => string | undefined | Promise<string | undefined>;

返回值

  • undefined:验证通过
  • string:验证失败,返回错误信息

验证规则示例

常用验证模式

长度验证

{
  type: 'string',
  name: 'username',
  label: '用户名',
  validator: ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    if (value.length < 3) return `\${label}至少3个字符`;
    if (value.length > 20) return `\${label}最多20个字符`;
    return undefined;
  }
}

格式验证

// 邮箱验证
{
  type: 'string',
  name: 'email',
  label: '邮箱',
  validator: ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return `\${label}格式不正确`;
    }
    return undefined;
  }
}

// URL 验证
{
  type: 'string',
  name: 'website',
  label: '网站',
  validator: ({ value, label }) => {
    if (value && !/^https?:\/\/.+\..+/.test(value)) {
      return `\${label}格式不正确`;
    }
    return undefined;
  }
}

// 手机号验证(中国大陆)
{
  type: 'string',
  name: 'phone',
  label: '手机号',
  validator: ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    if (!/^1[3-9]\d{9}$/.test(value)) {
      return `\${label}格式不正确`;
    }
    return undefined;
  }
}

// 国际手机号验证(如 +1-234-567-8901)
{
  type: 'string',
  name: 'internationalPhone',
  label: '国际手机号',
  validator: ({ value, label }) => {
    if (value && !/^\+?[\d\s\-()]{7,15}$/.test(value)) {
      return `\${label}格式不正确`;
    }
    return undefined;
  }
}

范围验证

{
  type: 'number',
  name: 'age',
  label: '年龄',
  validator: ({ value, label }) => {
    if (value && (value < 1 || value > 120)) {
      return `\${label}必须在1-120之间`;
    }
    return undefined;
  }
}

跨字段验证

{
  type: 'string',
  name: 'confirmPassword',
  label: '确认密码',
  validator: ({ value, label, data }) => {
    if (!value) return `\${label}不能为空`;
    if (value !== data.password) {
      return '两次输入的密码不一致';
    }
    return undefined;
  }
}

异步验证

基础用法

验证函数可以返回 Promise:

{
  type: 'string',
  name: 'username',
  label: '用户名',
  validator: async ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    
    // 检查用户名是否已存在
    const exists = await checkUsernameExists(value);
    if (exists) {
      return `\${label}已存在`;
    }
    
    return undefined;
  }
}

异步验证示例

const checkUsernameExists = async (username: string): Promise<boolean> => {
  // 模拟 API 调用
  await new Promise(resolve => setTimeout(resolve, 500));
  return username === 'admin';
};

{
  type: 'string',
  name: 'username',
  label: '用户名',
  validator: async ({ value, label }) => {
    if (!value) return `\${label}不能为空`;
    
    const exists = await checkUsernameExists(value);
    if (exists) {
      return `\${label}已被占用`;
    }
    
    return undefined;
  }
}

验证时机

自动验证

默认情况下,验证会在以下时机自动触发:

  1. 字段值变化时:当用户修改字段值后,自动验证该字段
  2. 依赖联动后:当依赖字段变化触发联动时,会同时验证相关字段
<Form 
  schema={schema}
  onChange={({ data, name }) => {
    // 值变化时自动触发验证
  }}
/>

手动验证

可以通过表单实例的 validate() 方法手动触发验证:

const formRef = useRef(null);

const handleSubmit = async () => {
  const { data, errors } = await formRef.current.validate();
  
  if (errors) {
    console.log('验证失败:', errors);
    // errors 格式: { fieldName: '错误信息' }
    return;
  }
  
  console.log('验证通过:', data);
};

<Form ref={formRef} schema={schema} />

validate() 方法详解

validate() 方法可以手动触发表单验证:

使用场景

// 验证所有字段
const result = await formRef.current.validate();

异常处理

验证器抛出的异常会被自动捕获并转换为错误信息:

{
  name: 'field',
  label: '字段',
  validator: ({ value }) => {
    // 抛出的异常会被捕获
    if (value === 'invalid') {
      throw new Error('Invalid value');
    }
    return undefined;
  }
}

// 异常会被转换为:
// errors: { field: 'Invalid value' }

## 错误提示

### 错误信息显示

验证失败时,错误信息可通过 `validate()` 方法获取:

```tsx
const formRef = useRef(null);
const [errors, setErrors] = useState<Record<string, string>>({});

const handleValidate = async () => {
  const result = await formRef.current.validate();
  setErrors(result.errors || {});
};

<Form ref={formRef} schema={schema} />

注意onChange 回调仅返回 { data, name, value },不包含 errors。如需实时获取验证结果,请调用 validate() 方法。

自定义错误样式

可以通过 Layout 配置自定义错误提示的样式:

const layout = {
  tipClassName: 'error-tip',
  tipStyle: {
    color: '#ff4d4f',
    fontSize: '12px',
    marginTop: '4px'
  }
};

验证规则组合

多个验证规则

可以为一个字段配置多个验证规则:

{
  type: 'string',
  name: 'password',
  label: '密码',
  required: true,
  validator: ({ value, label }) => {
    if (!value) return `${label}不能为空`;
    if (value.length < 6) return `${label}至少6个字符`;
    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
      return `${label}必须包含大小写字母和数字`;
    }
    return undefined;
  }
}

验证优先级

验证按以下顺序执行:

  1. 必填验证(required)
  2. 类型验证(type)
  3. 自定义验证(validator)

最佳实践

  1. 清晰的错误信息:提供明确、友好的错误提示
  2. 即时反馈:在用户输入时提供实时验证反馈
  3. 避免过度验证:只验证必要的字段
  4. 异步验证优化:对于异步验证,考虑添加防抖或节流
  5. 国际化支持:错误信息支持多语言