美文网首页
javaScript写一个验证类 (弃用if else式验证)

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

作者: 有头发的搬砖员 | 来源:发表于2019-08-08 11:29 被阅读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
            };
        }
    

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

    写完,收工。

    相关文章

      网友评论

          本文标题:javaScript写一个验证类 (弃用if else式验证)

          本文链接:https://www.haomeiwen.com/subject/tdtadctx.html