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

有项目经验的人都知道,数据验证是每个项目必做的工作
为何有这样的帖子呢?
是因为之前在看朋友的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
        };
    }

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

写完,收工。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 1、简介 Laravel 提供了多种方法来验证应用输入数据。默认情况下,Laravel 的控制器基类使用Valid...
    伊Summer阅读 1,525评论 0 3
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,233评论 0 4
  •   引用类型的值(对象)是引用类型的一个实例。   在 ECMAscript 中,引用类型是一种数据结构,用于将数...
    霜天晓阅读 1,054评论 0 1
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,756评论 2 9
  • Django 准备 “虚拟环境为什么需要虚拟环境:到目前位置,我们所有的第三方包安装都是直接通过 pip inst...
    33jubi阅读 1,326评论 0 5