javaScript写一个验证类 (弃用if else式验证)

2019-08-08  本文已影响0人  有头发的搬砖员

有项目经验的人都知道,数据验证是每个项目必做的工作
为何有这样的帖子呢?
是因为之前在看朋友的nodejs代码,他们的验证差不多是这样的

if(object.name == "" || object.name == null){
  .....
}else if(object.param == "" || object.param == null){
  .....
}else if....{
  .....
}

整个版面的代码,else if 验证占据60%的代码量
看到这样的代码,我的内心是崩溃的...


崩溃的模样

为何我们不能把验证做的漂亮点、舒服点呢?
说干就干,今天我们就用js写一个验证类,从此远离if else方式的验证

我们先定义一个需要验证的字段数据

var data = {
    id:1,
    name:'ken',
    age:29,
    sex:1,
    email:"open@163.com",
    explain:'',
};

嗯,没错这就是我们需要验证的字段
之后是我要对各个字段验证的规则,我期望是这样设计的

var rules = [ 
    {label:'id',ruleValidate:['required','isNumber']},
    {label:'name',ruleValidate:['required','length|min:2,max:10']},
    {label:'age',ruleValidate:['required','isNumber|max:10']},
    {label:'sex',ruleValidate:['required','in|str:1/2']},
    {label:'email',ruleValidate:['required','match|pattern:^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$']},
    {label:'explain',ruleValidate:['default|str:this is a boy!','length|max:500']}
];

▲ label跟字段保持一致,ruleValidate就是对各个字段的验证规则,验证规则是一个字符串数组。
▲ 一个字符串代表一个验证规则,例如required代表此字段必填。
▲ 使用 "|" 分割开验证参数,例如'length|min:2,max:10',代表字符串长度在2-10之间。

调用方法要尽量的简单,就像这样,两行的代码好了:

var model = new Model(); //new出一个验证对象
var _from = model.validate(data,rules); //进行验证

validate方法返回的_from 就是验证后from,里面会写入每个字段的验证错误信息。

好,现在我们开始设计这个验证类!!!!!!!!!!!

------------------------------------------我是分割线-----------------------------------------------

首先我们需要有一个类,当然还有一个外部方法validate,validate方法接收两个变量data和rules。

class model {
  validate(data,rules){
  }
}

我认为第一件事是先把数据合并起来 所以应该有个_mergeData函数,js类没有私有函数,所以我在私有函数前加上 _ 以作区分:

class model {
  _mergeData(data,rules){
    var result = [];  //返回的合并的数据
    for(var rule of rules){ //遍历rules
        if(!rule.hasOwnProperty('label')){ 
            throw new Error("rules has not 'label' Attribute");
        }
        var tempRoute={ 
            label:rule.label,               //复制label
            value:data[rule.label] || '',   //data数据写入value
            ruleValidate:rule.ruleValidate || [],   //复制ruleValidate字符串
            errors:[]                       //保存错误信息字段
        };
        result.push(tempRoute);
    }
    return result;
  }
  validate(data,rules){
    var _from = this._mergeData(data,rules);  //调用组合函数,得到from
  }
}

▲ 这里我希望合并后就是把data的数据内容放到对应的rules 里面的value变量里;
▲ 注意rules是引用传递过来,我不希望修改rules的内部数据,所以我要新建一个result 变量对data和rules是进行拷贝合并。

组合后的_from成了这个样子,这个需要脑补一下:

 [ 
    {label:'id',value:1,ruleValidate:['required','isNumber'],errors:[]},
    {label:'name',value:'ken',ruleValidate:['required','length|min:2,max:10'],errors:[]},
    {label:'age',value:29,ruleValidate:['required','isNumber|max:10'],errors:[]},
    {label:'sex',value:1,ruleValidate:['required','in|str:1/2'],errors:[]},
    {label:'email',value:"open@163.com",ruleValidate:['required','match|pattern:^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$'],errors:[]},
    {label:'explain',value:'',ruleValidate:['default|str:this is a boy!','length|max:500'],errors:[]}
];

得到这个数组后,接下来需要解释ruleValidate字段的验证规则,这里比较复杂,请眼睛跟着数字顺序走^ _ ^:

    /*
    * 把参数的字符串改转为键值对
    */
    _getParam(param){
        var paramArray = param.split(","); 
        // 7、参数以“,”分割,变成组数
        // min:2,max:10 转成 ['min:2','max:10']
        var result = {}
        for(var r of paramArray){  //8、遍历分割后的数组
          var key_value = r.split(":");  
          if(key_value.length == 2){
            result[key_value[0]] = key_value[1];
          }
        // 9、参数以“:”分割,组成键值对
        //  ['min:2','max:10']转成 {min:2,max:10}
        }
        return result;
    }
    /*
    * 把验证规则字符串改为规则
    */
    _getRuleList(ruleValidate){
        var result = [];
        for(var ruleStr of ruleValidate){
            var array = ruleStr.split("|"); //3、根据 “|”分解规则
            var temp = {
                ruleName:array[0],  //4、分解后前面的验证名放到ruleName变量里
                param:{},  //5、参数设置为一个空对象
            };
            if(array.length > 1){  //6、如果是有参数的,对参数解释
                temp.param = this._getParam(array[1]);
            }
            result.push(temp);
        }
        return result;
    }

    validate(data,rules){
        var _from = this._mergeData(data,rules);  //调用组合函数,得到from
        for(var fromObject of _from){
            if(fromObject.ruleValidate.length>0){ //1、确保有验证规则
                fromObject.ruleValidate = this._getRuleList(fromObject.ruleValidate); //2、把验证规则字符串改为规则列表
            }
        }
    }

这两个函数可以把规则列表转换成数组
例如 ['required','length|min:2,max:10'] 经过转换后变成

[
{ruleName:'required',param:{}},
{ruleName:'length',param:{min:2,max:10}
]

这些都转换好后,我们可以遍历这个列表对字段进行验证,现在我们需要一个validateValue方法,设计如下:

    /*
    * 验证字段
    */
    _validateValue(fromObject,ruleValidate){
        return eval(`this._${ruleValidate.ruleName}(fromObject,ruleValidate.param);`);
    }

    validate(data,rules){
        var hasError = false; //用一个布尔值保存整个表单的验证状态
        var _from = this._mergeData(data,rules);  //调用组合函数,得到from
        for(var fromObject of _from){
            if(fromObject.ruleValidate.length>0){ //确保有验证规则
                fromObject.ruleValidate = this._getRuleList(fromObject.ruleValidate); //把验证规则字符串改为规则列表
                for(var ruleValidate of fromObject.ruleValidate){
                    if(!this._validateValue(fromObject,ruleValidate)){
                        //fromObject整个表单
                        //ruleValidate其中一个验证
                        hasError = true;
                        //只要有一个表单是验证错误hasError 为true
                    }
                }
            }
        }
        //返回数据
        return {
            hasError:hasError,
            from:_from
        };
    }

▲ eval函数的作用是使用字符串当作函数名调用;
▲ 这里的this._${ruleValidate.ruleName}(fromObject,ruleValidate.param);
实际是调用ruleName内容前加上 _ 的函数名,如果ruleName的内容是required,那么函数名就是this._required;
▲ 验证函数统一接收两个参数,第一个是整个这一行表单,第二个是被转换后的验证参数。

注意:使用eval是为了简化代码,如果你的项目需要通过babel进行转换的,转换后为了压缩代码函数名会改变而导致报错,这里的eval可以改为使用switch去判断哪个函数调用。

▲ 我可以很清楚了解整个表单的验证通过情况,所以我增加了一个hasError 变量用于判断,如果_validateValue函数返回布尔值有一个是不通过的,hasError 则变成true,最终连同结果一起返回给调用方。

-----------------------------------是的,还是我分割线-----------------------------------------------
这样整个架构到这里就差不多了,现在我们可以着手去处理真正的验证部分。

在设计的时候,我们已经设计了required , isNumber , length等等一系列的名字,那我们只要在这个类里面增加_required() , _isNumber() , _length()这些方法来处理实际的验证并且返回值布尔值就是了,例如:

    _required(data,param){
        var value = data.value;
        if(value === "" || value == null || value == undefined){
          return false;
        }
        return true;
    }

这里先等一下,我们是不是缺少了些什么?
▲ 一个字段有这么多的验证,只有一个布尔值是不够的,我们必须反馈调用方到底都有什么验证不通过,不然调用方看着这个布尔值都摸不着头脑。
▲嗯?之前不是增加了errors字段吗?这里可以派上用场了。

    _required(data,param){
        var value = data.value;
        if(value === "" || value == null || value == undefined){
          data.errors.push(`${data.label}必须填写`);
          return false;
        }
        return true;
    }

返回信息是有了,但是不是有点死板呢?如果我想要个性化的错误提示方式怎么办?
▲我们可以增加一个message参数 ,在rules里面可以这样写:

{label:'id',ruleValidate:['required|message:亲,你这个ID怎么是没有','isNumber']},

▲当填写了message的时候就使用个性化的错误提示,如果没填写就采用默认的方式进行反馈。
▲另外,在我们判断字符是不是空的时候,我们也应该猜想,用户会不会输一堆空格给我呢?所以我们还要去掉字符的空格后再判断是否为空。

改写代码如下:

    /*
    * 去掉所有空格
    */
    _trim(value){
        //判断如果变量是数字,或者是空的直接返回
        if(typeof(value) == 'number' || value === undefined){
            return value;
        }
        return value.replace(/\s+/g,"");
    }
    /*
    * 取得验证错误信息
    */
    _getErrorMessage(defaultMsg,param){
        if(param && param.hasOwnProperty('message')){
            //param 有message参数的时候返回这个内容
            return param.message;
        }
        //否则返回默认错误信息
        return defaultMsg;
    }
    
    /*
    * 必填项
    */
    _required(data,param){
        var value = this._trim(data.value);
        if(value === "" || value == null || value == undefined){
          data.errors.push(this._getErrorMessage(`${data.label}必须填写`,param));
          return false;
        }
        return true;
    }

设计也差不多了,以下是其他的验证方式,写法也差不多:

    /*
    * 是否为数字及数值范围
    * 'isNumber|min:2,max:10'
    */
    _isNumber(data,param){
        var value = data.value;
        var re = /^(\+|-)?\d+($|\.\d+$)/;
        if(!re.test(value)){
            data.errors.push(this._getErrorMessage(`${data.label}必须是数字`,param));
            return false;
        }
        else if(param.hasOwnProperty('min') && value < param.min){
            data.errors.push(this._getErrorMessage(`${data.label}不能少于${param.min}`,param));
        }
        else if(param.hasOwnProperty('max') && value > param.max){
            data.errors.push(this._getErrorMessage(`${data.label}不能大于${param.max}`,param));
        }
        return true;
    }
    
    /*
    * 字符长度
    * 'length|min:2,max:10'
    */
    _length(data,param){
        var value = data.value;
        if(param.hasOwnProperty('min') && value.length < param.min){
          data.errors.push(this._getErrorMessage(`${data.label}长度不能少于${param.min}`,param));
          return false;
        }
        else if(param.hasOwnProperty('max') && value.length > param.max){
          data.errors.push(this._getErrorMessage(`${data.label}长度不能超过${param.max}`,param));
          return false;
        }
        return true;
    }
    
    /*
    * 字符为空时默认值
    * 'default|str:abcdefg'
    */
    _default(data,param){
        var value = this._trim(data.value);
        if(value === "" || value == null || value == undefined){
            if(param.hasOwnProperty('str')){
                data.value = param.str
            }
        }
        return true;
    }
    
    /*
    * 字符在填写范围内
    * 'in|str:abc/def/g'
    */
    _in(data,param){
        var value = this._trim(data.value);
        if(param.hasOwnProperty('str')){
          var string = param.str.split("/");
          for(var key in string){
              var str = string[key];
              if(str == value){
                return true;
              }
          }
        }
        data.errors.push(this._getErrorMessage(`${data.label}不在填写范围内`,param));
        return false;
    }
    
    /*
    * 正则表达式
    * 'match|pattern:^[\u4e00-\u9fa5_0-9_a-z_A-Z*#\'\\-\(\)\. ]+$'
    */
    _match(data,param){
        var value = data.value;
        if(param.hasOwnProperty('pattern')){
          var pattern = new RegExp(param.pattern);
          if(!pattern.test(data.value)){
            data.errors.push(this._getErrorMessage(`${data.label}不符合填写规范`,param));
            return false;
          }
        }
        return true;
    }

▲如果你喜欢,可以自行加上email、电话或者身份证等等的验证规则,根据自己的业务需求丰富的通用验证类,从而可以抛弃if else式的验证,但仅仅这样就够了吗?


发自内心的疑惑

有过实际项目经验的人都知道,仅仅只有通用验证是远远不够的,这时候我们可以修改的设计,让其兼容一些自定义的函数验证......

-----------------------------前方高能,你猜对了,又是我分割线-------------------------------

首先我们有一个自定义的验证函数,像这样:

var jsons = [{id:1,name:"ken"},{id:2,name:"ryu"}];
var callbackTest = function (from,param,data){
    for(var json of jsons){
        if( json.id == data.id  &&  json.name  == data.name ){
            return true;
        }
    }
    from.errors.push('不存在json数组内');
    return false;
};

▲这个验证函数是调用方写的函数,我们希望在调用的时候可以用上,那么我们需要修改这个验证类,让其能识别。
▲首先我们需要在提交验证规则的时候把函数一并提交过去,并且使用上

var rules = [ 
    {label:'id',ruleValidate:['required','isNumber']},
    {label:'name',ruleValidate:['required','length|min:2,max:10','myCustom'],custom:{
        myCustom:callbackTest
    }},
//增加了custom字段
    {label:'age',ruleValidate:['required','isNumber|max:10']},
    {label:'sex',ruleValidate:['required','in|str:1/2']},
    {label:'email',ruleValidate:['required','match|pattern:^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$']},
    {label:'explain',ruleValidate:['default|str:this is a boy!','length|max:500']},
];

▲在规则内增加了custom字段,字段内有个myCustom变量,指向了我们的自定义验证函数callbackTest
▲['required','length|min:2,max:10','myCustom'],表明了在required和length之后,我们会调用myCustom的规则
▲那么我们需要修改_mergeData函数,把custom字段一并复制上去

    /*
    * 合并数据
    */
    _mergeData(data,rules){
        var result = [];
        for(var rule of rules){
            if(!rule.hasOwnProperty('label')){
                throw new Error("rules has not 'label' Attribute");
            }
            var tempRoute={
                label:rule.label,
                value:data[rule.label] || '',
                ruleValidate:rule.ruleValidate || [],
                custom:rule.custom || {}, //新增的custom字段
                errors:[]
            };
            result.push(tempRoute);
        }
        return result;
    }

▲之后我们需要修改一下验证函数,因为自定义函数体内有data参数,修改如下:

/*
    * 验证字段
    */
    _validateValue(fromObject,ruleValidate,data){
        //增加data形参
        //判断custom内是否有这个函数名,如果有则优先调用
        if(fromObject.custom.hasOwnProperty(ruleValidate.ruleName) && typeof(fromObject.custom[ruleValidate.ruleName]) == 'function' ){
            return fromObject.custom[ruleValidate.ruleName](fromObject,ruleValidate.param,data);
        }else{
            return eval(`this._${ruleValidate.ruleName}(fromObject,ruleValidate.param);`);
        }
    }
    
    /*
    * 验证方法 (对外接口)
    */
    validate(data,rules){
        var hasError = false;
        var _from = this._mergeData(data,rules);
        for(var fromObject of _from){
            if(fromObject.ruleValidate.length>0){
                fromObject.ruleValidate = this._getRuleList(fromObject.ruleValidate);
                for(var ruleValidate of fromObject.ruleValidate){
                    //增加传入data
                    if(!this._validateValue(fromObject,ruleValidate,data))
                    {

                        hasError = true;
                    }
                }
            }
        }
        return {
            hasError:hasError,
            from:_from
        };
    }

只要做这样的修改,就能实现引入外部回调函数。
除此以外还有什么特别的功能?
这样做,外部还可以使用同名的验证函数来覆盖内部的这些通用验证方法

写完,收工。

上一篇下一篇

猜你喜欢

热点阅读