有项目经验的人都知道,数据验证是每个项目必做的工作
为何有这样的帖子呢?
是因为之前在看朋友的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
};
}
只要做这样的修改,就能实现引入外部回调函数。
除此以外还有什么特别的功能?
这样做,外部还可以使用同名的验证函数来覆盖内部的这些通用验证方法
写完,收工。