async-validator 源码解析(一) - 基本使用

1,702 阅读4分钟

async-validator 是一个表单异步校验库,npm上每周下载量达120万次左右,代码设计十分优秀,这期我准备来详情解读一下源码,从中学习和了解作者的设计思想,便于在项目中更灵活的运用。

在小程序开发中用到了 uviewform 表单组件,在开发的过程中总是遇到各种验证问题,如配置的规则不生效、重置表单不刷新验证状态、设置自定义的验证规则、动态设置验证规则不生效等问题。所以简单了看了一下 uviewform 表单组件的内部实现,发现代码量很少,实现的非常简洁,之所以能简单实现,主要还把表单的验证逻辑独立出来了入到 async-validator.js 中,虽然没有引用 npm 上的 async-validator,是因为有一些个性化的定制,所以放到 libs/util 目录下单独维护,但其核心实现就是 async-validator,所以我们可以先解析 npm 上应用最为广泛的 async-validator 模块的源码,再回过头来看 uview 组件库中的改动。

截止到现在,async-validator 的版本为 4.2.5, 每周下载量约120万左右,我们就看这个版本的源码内容

功能特点

  • 可以指定类型(有内置15种类型)
  • 可以自定义同步的 validator 和 异步的 asyncValidator
  • 可以自定义 messagemessage 可以是函数,可以配置 message 默认值用于国际化
  • 可以指定正则表达式
  • 可以配置转换器,在校验前转换数值
  • 可以定义嵌套规则
  • 可以设置校验方式,如同步校验所有规则,还是顺序校验有错时停止校验
  • 字符串、数组、数值可以设置 len min max 校验长度和范围

使用示例

import Schema, {
  Rules,
  ValidateCallback,
  ValidateFieldsError,
  Values,
} from 'async-validator';

const rules:Rules = {
  // 指定类型 number
  age: {
    required: true,
    type: 'number',
  },
  // 自定义validator
  name: {
    type: 'string',
    required: true,
    // 返回 boolean
    validator: (rule, value) => value === 'muji',
  },
  // 自定义 异步的validator
  height: {
    type: 'number',
    // 返回 promise
    asyncValidator: (rule, value) => {
      return new Promise<void>((resolve, reject) => {
        if (value < 18) {
          reject('too young');  // reject with error message
        } else {
          resolve();
        }
      });
    },
  },
  // 使用内置类型校验,添加多个验证规则
  // 内置类型:string/number/boolean/method/regexp/integer/float/array/object/enum/date/url/hex/email/any
  email: [
    {
      type: 'email',
      required: true,
    },
    {
      validator(rule, value, callback, source, options) {
        const errors = [];
        // 测试email地址是否已经在数组库中存在
        // 并当已存在时在errors数组中添加一个error
        return errors;
      },
    },
  ],
  // 使用正则校验,并在校验前 转换原数值
  subname: {
    type: 'string',
    required: true,
    pattern: /^[a-z]+$/,
    transform(value) {
      return value.trim();
    },
  },
  // 自定义 messaage, 可以是一个函数
  firstname: { 
    type: 'string', 
    required: true, 
    message: () => 'name is required'
  },
  // 可以校验一个枚举值
  role: { 
    type: 'enum', 
    enum: ['admin', 'user', 'guest'] 
  },
  // 可以设置嵌套校验规则
  address: {
    type: 'object',
    required: true,
    options: { first: true },
    fields: {
      street: { type: 'string', required: true },
      city: { type: 'string', required: true },
      zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
    },
  },
  // 可用于校验 radio 或 checkbox的选中数值
  isVip: {
    type: 'enum', 
    enum: [0, 1],
  }
}

const schema = new Schema(rules);

const source = {
  age: '',
  name: '',
  height: 0,
  email: '',
  subname: '',
  firstname: '',
  role: '',
  address: {},
  isVip: 1
}

// 回调方式
schema.validate(source, (errors, fields) => {
  if(errors){
    console.log(errors, fields);
    return;
  }

  console.log(fields)
});

// Promise 方式
schema.validate(source).then((source) => {
    console.log('success')
}).catch(({errors, fields}) => {
    console.log(errors, fields);
})

返回的错误和字段信息

// errors
[
  { message: 'age is required', fieldValue: '', field: 'age' },
  { message: 'name fails', fieldValue: '', field: 'name' },
  { message: 'email is required', fieldValue: '', field: 'email' },
  { message: 'subname is required', fieldValue: '', field: 'subname' },
  { message: 'name is required', fieldValue: '', field: 'firstname' },
  {
    message: 'role must be one of admin, user, guest',
    fieldValue: '',
    field: 'role'
  },
  {
    message: 'address.street is required',
    fieldValue: undefined,
    field: 'address.street'
  },
  { message: 'too young', fieldValue: 0, field: 'height' }
]

// fields
{
  age: [ { message: 'age is required', fieldValue: '', field: 'age' } ],
  name: [ { message: 'name fails', fieldValue: '', field: 'name' } ],
  email: [ { message: 'email is required', fieldValue: '', field: 'email' } ],
  subname: [
    {
      message: 'subname is required',
      fieldValue: '',
      field: 'subname'
    }
  ],
  firstname: [
    { message: 'name is required', fieldValue: '', field: 'firstname' }
  ],
  role: [
    {
      message: 'role must be one of admin, user, guest',
      fieldValue: '',
      field: 'role'
    }
  ],
  'address.street': [
    {
      message: 'address.street is required',
      fieldValue: undefined,
      field: 'address.street'
    }
  ],
  height: [ { message: 'too young', fieldValue: 0, field: 'height' } ]
}

rule 的可配置选项

src/interface.ts

export interface RuleItem {
  // 类型
  type?: RuleType; // default type is 'string'
  // 是否必填
  required?: boolean;
  // 正则
  pattern?: RegExp | string;
  // 最小值
  min?: number; // Range of type 'string' and 'array'
  // 最大值
  max?: number; // Range of type 'string' and 'array'
  // 长度
  len?: number; // Length of type 'string' and 'array'
  // 枚举值,值必是枚举值中的一种
  enum?: Array<string | number | boolean | null | undefined>; // possible values of type 'enum'
  // 是否校验空白符
  whitespace?: boolean;
  // 嵌套规则定义,如 { adress: { street, city, zip}}
  fields?: Record<string, Rule>; // ignore when without required
  // 额外的配置  {}
  options?: ValidateOption;
  // 用于统一定义对象或数组中每个元素的校验规则
  defaultField?: Rule; // 'object' or 'array' containing validation rules
  // 转换器,用于在校验前转换数据
  transform?: (value: Value) => Value;
  // 校验失败的消息
  message?: string | ((a?: string) => string);
  // 自定义的异步检验器
  asyncValidator?: (
    rule: InternalRuleItem,
    value: Value,
    callback: (error?: string | Error) => void,
    source: Values,
    options: ValidateOption,
  ) => void | Promise<void>;
  // 自定义的同步校验器
  validator?: (
    rule: InternalRuleItem,
    value: Value,
    callback: (error?: string | Error) => void,
    source: Values,
    options: ValidateOption,
  ) => SyncValidateResult | void;
}

特殊的几个配置项

whitespace

一般如果字符串全部是空格的必填字段会视为错误,如果希望字符串是空格但希望校验通过,那么需要设置为 true

fields & defaultField

这两个配置都用于 objectarray 的嵌套校验 fields 用于设置每一个字段或元素的校验规则 defaultField 用于设置全部字段或元素的校验规则

address: {
  type: 'object',
  required: true,
  options: { first: true },
  fields: {
    street: { type: 'string', required: true },
    city: { type: 'string', required: true },
    zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
  },
},
roles: {
  type: 'array',
  required: true,
  len: 3,
  fields: {
    0: { type: 'string', required: true },
    1: { type: 'string', required: true },
    2: { type: 'string', required: true },
  },
},
urls: {
  type: 'array',
  required: true,
  defaultField: { 
    type: 'url' 
  },
},

transform

用于在校验前转换数据或格式化,比如去除字符两端的空白符

 name: {
    type: 'string',
    required: true,
    pattern: /^[a-z]+$/,
    transform(value) {
      return value.trim();
    },
  },

options

  • suppressWarningboolean,是否禁止显示有关无效值的内部警告。
  • firstboolean,当第一个验证规则生成错误时调用,不再处理任何验证规则。如果验证涉及多个异步调用(例如,数据库查询),并且只需要第一个错误,请使用此选项。
  • firstFieldsboolean|String[],当指定字段的第一个验证规则生成错误时调用,不再处理同一字段的验证规则。true 表示所有字段都应用这个规则(只要遇到规则未通过,就不继续校验该字段后面的规则)。

默认的 message 配置

src/messages.ts

{
    default: 'Validation error on field %s',
    required: '%s is required',
    enum: '%s must be one of %s',
    whitespace: '%s cannot be empty',
    date: {
      format: '%s date %s is invalid for format %s',
      parse: '%s date could not be parsed, %s is invalid ',
      invalid: '%s date %s is invalid',
    },
    types: {
      string: '%s is not a %s',
      method: '%s is not a %s (function)',
      array: '%s is not an %s',
      object: '%s is not an %s',
      number: '%s is not a %s',
      date: '%s is not a %s',
      boolean: '%s is not a %s',
      integer: '%s is not an %s',
      float: '%s is not a %s',
      regexp: '%s is not a valid %s',
      email: '%s is not a valid %s',
      url: '%s is not a valid %s',
      hex: '%s is not a valid %s',
    },
    string: {
      len: '%s must be exactly %s characters',
      min: '%s must be at least %s characters',
      max: '%s cannot be longer than %s characters',
      range: '%s must be between %s and %s characters',
    },
    number: {
      len: '%s must equal %s',
      min: '%s cannot be less than %s',
      max: '%s cannot be greater than %s',
      range: '%s must be between %s and %s',
    },
    array: {
      len: '%s must be exactly %s in length',
      min: '%s cannot be less than %s in length',
      max: '%s cannot be greater than %s in length',
      range: '%s must be between %s and %s in length',
    },
    pattern: {
      mismatch: '%s value %s does not match pattern %s',
    },
}

自定义 validator 的返回值

  • asyncValidator异步校验器,必须返回一个 promise 对象,如果是 reject,可以传递一个 Error 对象
  • validator 同步校验器,可以返回 truefalse, 也可以返回一个 Error 对象,callback 也可以传递一个 Error 对象和直接返回一个 Error 对象效果相同,如果返回 flase,会使用自定义的 message,如果没有,则使用内置的 message
v2: [
  {
    asyncValidator(rule, value) {
      return Promise.reject(new Error('e3'));
    },
  },
],
v3: [
{
    validator(rule, value, callback) {
        callback(new Error('e1'));
    },
},
{
    validator() {
      return new Error('e5');
    },
},
{
    validator() {
      return false;
    },
    message: 'e6',
},
{
    validator() {
      return true;
    },
},
],

总结

async-validator 经过几年的迭代,可以看出来是一个非常强大且成熟的验证器,几乎满足99%的验证场景,再结合组件库封装添加一些个性化UI的功能,就已经是非常完美了,实事也是如此,在看了antd、elementUI、iView等组件库表单验证的实现,无一不是使用了 async-validator,在日常开发中,表单验证的业务场景太多,有必要深入了解一下。