validatorjs 简单,灵活,多功能的表单校验库,源码开发思路分享

829 阅读5分钟

validatorjs

一款简单,灵活,多功能的表单校验库

支持必填限制、类型校验、自定义正则、自定义校验函数、自定义message钩子等;封装了一系列基础的格式校验函数。


基于怎么用,再去怎么实现;参考了async-validator库的接口设计,融入了一些自己的想法;用到设计模式做了一些优化。

使用介绍:

const rules = {
  name: { required: true, message: "请输入姓名" },
  phone: [
    { required: true, message: "请输入手机号码" },
    // 这里用Validator上的预设正则,也可以自定义正则
    { pattern: Validator.pattern.phone, message: "请输入正确的手机号码" },
  ],
};

const form = {
  name: "",
  phone: "156",
};

const validator = new Validator({
  rules,
});

// 所有字段校验
validator
  .validate(form)
  .then(() => {
    console.log("validate success");
  })
  .catch((errs) => {
    console.log("validate fail");
    /**
     * 输出:
     *  {
     *    name: { required: true, message: '请输入姓名' },
     *    phone: { pattern: Validator.pattern.phone, message: '请输入正确的手机号码' }
     * }
     */
    console.log(errs);
  });

// 指定字段校验
validator
  .validateField("phone", form)
  .then(() => {
    console.log("validate success");
  })
  .catch((err) => {
    console.log("validate fail");
    /**
     * 输出:
     * { pattern: Validator.pattern.phone, message: '请输入正确的手机号码' }
     */
    console.log(err);
  });

最基本的使用如上,然而这只是冰山一角

关于 rules 的详细配置:

格式或被md识别为表格列间隔符了,所以用/代替

属性说明类型
required是否必填Boolean
type类型校验,可设置:string/number/boolean/function/float/integer/array/object/date/regexpString
pattern正则校验Regexp
validator自定义校验函数,支持异步,返回一个Promise实例;可以通过Promise.reject()或抛出错误来自定义message() => Boolean/Promise<undefined/string>/never
maxlength最大长度Number
minlength最小长度Number
enum枚举Array
message错误信息,如果是函数,则会被当成钩子被执行String/Function

message钩子:

有时我们需要在验证不通过时,弹出提示,这时就可以使用message钩子

const rules = {
  name: { required: true, message: '请输入姓名' }
}

const validator = new Validator({
  rules,
  // 校验不通过则会调用该钩子函数
  messageHook(message) {
    console.log(message);
  }
})

validator.validate({
  name: null
}).then(() => {
  console.log('validate success');
}).catch(errs => {
  console.log('validate fail');
})

/**
  * 控制台打印:
  *  validate fail
  *  请输入姓名
*/

单独配置某个规则的message钩子:

const rules = {
  name: { 
    required: true, 
    // message可以写成一个函数
    message() {
      console.log('请输入姓名');
    }
  }
}

validator.validate({
  name: null
}).then(() => {
  console.log('validate success');
}).catch(errs => {
  console.log('validate fail');
})

/**
  * 控制台打印:
  *  validate fail
  *  请输入姓名
*/

transform(验证前处理值):

有时我们将变量绑定了数值输入框,然而值是string类型,这时如果配置了type: 'number',则无论如何都会校验不通过(当然vue可以使用v-model.number来解决这个问题); 又或是某个字符串值需要进行trim后进行验证,所以都需要transform配置的存在;

const rules = {
  name: [
    { required: true, message: '请输入姓名' },
    { maxlength: 4, message: '姓名最长4位' }
  ],
  age: { type: 'number', message: '请输入正确的年龄' }
}

const transform = {
  name: value => value.trim(),
  age: value => Number(value)
}

const validator = new Validator({
  rules,
  transform
})

validator.validate({
  // 通过transform转换,name和age都会被校验通过,
  name: 'liyu ',
  age: '18'
}).then(() => {
  console.log('validate success');
}).catch(errs => {
  console.log('validate fail');
})

使用介绍如上,更详细内容及源码请访问gitee仓库: gitee.com/ytiona/vali…


源码开发记录:

最基础的几个功能源码:

required\type\pattern\validator

 class Validator {
  _rules = {};
  constructor(rules) {
    Object.keys(rules).forEach(field => {
      const item = rules[field];
      // 格式统一
      this._rules[field] = Array.isArray(item) ? item : [item];
    });
  }
  validate(form) {
    const { _rules } = this;
    const tasks = Object.keys(_rules).map(field => this.validateField(field, form));
    return new Promise(async (resolve, reject) => {
      const validateResult = await Promise.allSettled(tasks);
      // 过滤出验证失败的项
      const errors = validateResult.filter(item => item.status === 'rejected');
      if (errors.length > 0) {
        const errorsMap = {};
        errors.forEach(err => {
          // 字段名映射
          errorsMap[err.reason.field] = err.reason.rule;
        })
        return reject(errorsMap);
      }
      return resolve();
    })
  }
  validateField(field, form) {
    const { _rules } = this;
    // 如果规则中不存在,则认定为校验通过
    if(!_rules[field]) return Promise.resolve();
    return new Promise(async (resolve, reject) => {
      const currentRules = _rules[field];
      for (let i = 0, len = currentRules.length; i < len; i++) {
        const rule = currentRules[i];
        const { oneOf, isEmpty, capitalize, isRegexp, isFunction } = Validator;
        if(rule.required) {
          if(isEmpty(form[field])) {
            return reject({ field, rule });
          }
        }
        if(rule.type) {
          const { type } = rule;
          // 有效的type,添加对应的类型校验
          if(oneOf(type, Validator.types)) {
            // 使用Validator类上静态方法,类型校验
            if(!Validator[`is${capitalize(type)}`](form[field])) {
              return reject({ field, rule });
            }
          } else {
            console.warn(`There is a type in field ${field} that is unsupported`);
          }
        }
        if(rule.pattern) {
          const { pattern } = rule;
          // 有效的正则,添加正则校验
          if(isRegexp(pattern)) {
            if(!pattern.test(form[field])) {
              return reject({ field, rule });
            }
          } else {
            console.warn(`There is a pattern in field ${field} that is not of type regexp`);
          }
        }
        if(rule.validator) {
          const { validator } = rule;
          // 自定义校验
          if(isFunction(validator)) {
            try {
              const validRes = validator(form[field]);
              if(validRes instanceof Promise) {
                // 如果validRes是promise.reject则会被下面catch捕获
                await validRes;
              } else if(!validRes) {
                // 自定义validator校验不通过
                return reject({ field, rule })
              }
            } catch (err) {
              let errMsg = err;
              // 取error对象中的message或reject对象中的message
              if(typeof(err) === 'object') {
                errMsg = err.message;
              }
              // 捕获自定义validator中的异常和Promise.reject
              return reject({
                field,
                rule: {
                  ...rule,
                  message: errMsg || rule.message
                }
              });
            }
          } else {
            console.warn(`There is a validator in field ${field} that is not of type function`);
          }
        }
      }
      // 如果走到这里没有被reject掉,则代表校验通过
      return resolve();
    })
  }
}

可以看到validateField里面为了判断规则配置,写了一堆的if,而且逻辑全部都堆在了这个函数中,后续维护起来会非常麻烦,比如增加一个校验配置,又或是改变校验规则的优先级顺序。

所以就想着用设计模式来优化一下,第一时间想到的是策略模式,但似乎暴露给外部的 rules 配置,就是策略模式的一种应用,里面某个规则的实现就是具体的策略;

仔细一想,像这种规则一个个串联起来,只要有一个校验不通过,则会终止,好像和职责链模式挺像的,话不多说,开干。

使用职责链模式优化:

class Validator {

  validateField(field, form) {
    const { _rules } = this;
    // 如果规则中不存在,则认定为校验通过
    if(!_rules[field]) return Promise.resolve();
    return new Promise(async (resolve, reject) => {
      const currentRules = _rules[field];

      for(const rule of currentRules) {
        // 创建职责链
        const chain = [
          this._validateRequired,
          this._validateType,
          this._validatePattern,
          this._customValidate
        ]
        // 执行职责链
        for(const validator of chain) {
          const validateRes = await validator(rule, form[field], field);
          let errMsg;
          let pass = validateRes;
          // 兼容校验函数抛出自定义message
          if(typeof(validateRes) === 'object') {
            pass = validateRes.pass;
            errMsg = validateRes.message;
          }
          // 校验不通过,中断职责链,返回校验结果
          if(!pass) {
            return reject({ 
              field, 
              rule: {
                ...rule,
                message: errMsg || rule.message
              }
            });
          }
        }
      }
      return resolve();
    })
  }

  // 必填校验
  _validateRequired(rule, value): boolean { ... }

  // 类型校验
  _validateType(rule, value, field): boolean { ... }

  // 正则校验
  _validatePattern(rule, value, field): boolean { ... }

  // 自定义校验,返回值特定情况需要特殊处理,因为有时候需要自定义message
  async _customValidate(rule, value, field): boolean | Promise<undefined | string> { ... }
}

可以看到validateField中的校验都被各自封装了,只需要保证他们返回的结果一致; 这样就可以在validateField中做职责链的中断处理,而校验函数只负责接收参数,返回校验结果

经过设计模式改造后,如果需要增加判断规则,只需再对其封装,然后chain变量中添加就行了; 如果需要改变校验规则的优先级,则只需调整他们在数组中的顺序就可以了。

设计模式优化后,增加maxlength、minlength、enum校验:

实际上这些校验都可以通过自定义validator来实现,但是感觉违背了封装的初衷;

class Validator {
  validateField(field, form) {
    const { _rules } = this;
    // 如果规则中不存在,则认定为校验通过
    if(!_rules[field]) return Promise.resolve();
    return new Promise(async (resolve, reject) => {
      const currentRules = _rules[field];

      for(const rule of currentRules) {
        // 创建职责链
        const chain = [
          this._validateRequired,
          this._validateType,
          this._validatePattern,

          // 这里改动的只增加了这三行代码
          // 且这三个校验优先于自定义校验
          this._validateMaxlen,
          this._validateMinlen,
          this._validateEnum,

          this._customValidate
        ]
        // 执行职责链
        for(const validator of chain) {
          const validateRes = await validator(rule, form[field], field);
          let errMsg;
          let pass = validateRes;
          // 兼容校验函数抛出自定义message
          if(typeof(validateRes) === 'object') {
            pass = validateRes.pass;
            errMsg = validateRes.message;
          }
          // 校验不通过,中断职责链,返回校验结果
          if(!pass) {
            return reject({ 
              field, 
              rule: {
                ...rule,
                message: errMsg || rule.message
              }
            });
          }
        }
      }
      return resolve();
    })
  }
    // 最大长度校验
  _validateMaxlen(rule, value, field) {
    const { maxlength } = rule;
    if(maxlength) {
      if(Validator.isInteger(maxlength) && maxlength > 0) {
        return value.length <= maxlength;
      }
      console.warn(`There is a maxLength in the field ${field} that is not a positive integer type`)
    }
    return true;
  }

    // 最小长度校验
  _validateMinlen(rule, value, field) { ... }
  
  // 枚举校验
  _validateEnum(rule, value, field) {...}
}

可以看到,增加了三个规则,并没有对validateField做太多改动。

其余关于transform、messageHook的扩展,感兴趣的可以看源码: gitee.com/ytiona/vali…

参考资料:

1.github.com/yiminghe/as…;

2.《JavaScript 设计模式与开发实践》;