原生js设计表单验证插件的思路分析

1,619 阅读6分钟
这几天在做一个用原生js写的项目,需要用到表单验证的功能。因为之前公司项目中的表单验证是写在业务里的,改起来特别的麻烦,就想自己写一个表单验证的小工具。本来想在网上找一个教程研究研究的,但没找到太好的,最后决定自己研究吧。文中示例的代码都是我自己写的demo,并没有参考一些框架或者库的源码,所以代码可能比较难看,重点还是分析一下思路,后期我会把完善好的代码发到github上。

原则:

先说一下我自己写的表单验证工具的思路或者是原则吧:

  • 验证代码和业务代码分离
  • 扩展性强
  • 验证规则和验证逻辑分离

第一点没什么可说的,一般的工具都是这样做的,而且自己也吃了不少高耦合的亏,所以我还是把它放出来了。我这个工具希望达到的目的是,开发者只需要关注哪些数据需要验证,将数据传入给工具直接获得验证结果,让开发者更多的关注其他的业务。

第二点是希望开发者可以自定义验证规则,毕竟内置的规则再多,也架不住一个奇葩需求。

第三点是验证规则和验证逻辑的分离,其实这一条更多的是为第二条服务的。

验证逻辑


这张流程图就是我这个工具验证逻辑,最开始判断的传入值是否可以为空,如果可以为空再去验证是否有其他的验证规则,如果没有就直接验证通过了(一般来说,如果表单的值可以为空,大部分情况不会再存在其他规则了)。假如还有其他的验证规则,就去循环验证,一旦有一条规则没通过,就算验证失败。

如果说输入的内容不可以为空,就去循环验证这些规则就好了,和上面同理,一旦有错就算验证失败。

class Vaildation{}

这就是验证工具的类,我们先不着急往下写,先思考到底往类中传什么配置参数。

let vaild = [
  {
    value:this.username,
    type: "用户名",
    rules:[
      "isDefine", 
      {
        name:"limit",
        check: true,
        min:5,
        max:12
      }
    ]
  },
  {
    value:this.password,
    type: "密码",
    rules:[
      "isDefine", 
      {
        name:"mix",
        check:true,
      },
      {
        name:"limit",
        check: true,
        min:5,
        max:12
      }
    ]
  }
];

这是我想传到类中的配置(如果你自己想设计一个的话也可以用别的样式,数据、对象都可以),它是一个数组,里面包含了每一个需要验证的对象,简单介绍一下配置中的属性:

value: 需要认证的数据的值,类型是string或者number
type:  传入这个数据的名称或者标签,比如用户名、密码、邮箱或者电话等,类型是字符串或者数组
rules: 需要使用哪些验证规则,类型是array。

rules有两种写法,一种是简写比如:['isDefine', 'limit'],意思是不能为空,且有字数限制,使用默认
的规则;
也可以自定义具体的规则比如:['isDefine', {name: limit, check:true, min:5, max:12}], check参数
的意思是使规则生效,如果你写成false即使你写上这个规则也不会生效,min和max就好理解了,就是限制具体的
字数,默认的是6-16个字符。

让我们接着回到类的设计中:

class Vaildation{
  constructor(){    
    this.isCheck = false;    
    this.vaild_item = [];    
    this.vaild;  
  }
}

isCheck: 验证是否通过,类型是布尔值
vaild_item: 存放我们传入类中的参数
vaild: 验证成功或失败时返回的消息

接下来,让我们来设计验证规则rules:

class Vaildation{
  constructor(){    
    ......  
  }  
  rules(){    
    return {      
      mix: (vaild) => {        
        for(let i = 0; i < vaild.rules.length; i++){          
          if(vaild.rules[i] == "mix" || vaild.rules[i].name == "mix"){            
            if(vaild.rules[i].check){              
              if(!vaild.rules[i].newRules){                
                return new RegExp(/^[a-zA-Z0-9]*([a-zA-Z][0-9]|[0-9][a-zA-Z])[a-zA-Z0-9]*$/).test(vaild.value);               
              }else{                
                return new RegExp(vaild.rules[i].newRules).test(vaild.value);              
              }            
            }else{              
              return true;            
            }          
          }       
        }      
      },      
      limit: (vaild) => {        
        let min;        
        let max;        
        vaild.rules.forEach((item) => {          
          if(item == "limit" || item.name == "limit"){            
            min = item.min || 6;            
            max = item.max || 16;          
          }        
        })        
        if(vaild.value.length < min || vaild.value.length > max){          
          return false;        
        }else{          
          return true;        
        }      
      },      
      isDefine: (vaild) => {        
        return !!vaild.value.trim();      
      },    
    }  
  }
}

rules方法中返回了一个规则对象,我这里内置了两个规则:

一个是"mix",规定输入的内容是数字和字母的混合;

另一个"limit",规定输入内容字符数量的限制;

它们的参数是之前配置数组中的rules属性的值,即["isDefine", "mix", "limit"],我们可以看到这些规则是支持传入字符串或者是对象的,传入对象时还可以做一些额外的配置,比如当我们有一些奇葩需求比如“输入的内容每隔3个字必须有一个字母(for god's sake,天杀的需求)”,我们可以在"mix"中加入新的正则表达式:

{  
  name:"mix",  
  check:true,
  newRules: 新的正则表达式,
},

当然,如果你有新的规则比如电话,邮箱之类的,你也可以在新建Vaildation实例后手动添加进去,方便自定义:

let vaildation = new Vaildation();
vaild.rules()[newRules] = function(vaild){
    //new Rules
}

有了规则,我们还需要错误提示,我这里每个规则的提示都使用了和规则一样的名称,方便处理验证逻辑时使用:

error(){
    return {
      mix: (vaild)=>{
        return vaild.type + "必须为字母和数字组合";
      },
      limit: (vaild) => {
        let min;
        let max;
        vaild.rules.forEach((item) => {
          if(item == "limit" || item.name == "limit"){
            min = item.min || 6;
            max = item.max || 16;
          }
        })
        return vaild.type + "位数为" + min + "-" + max + "位";
      },
      define: (vaild) => {
        return vaild.type + "不能为空";
      }
    }
  }

处理错误的时候我们就用到配置中的type属性了,当然也可以直接在error中写死错误处理的文案,这些都无所谓拉。

最后我们要看的是验证的逻辑处理:

check_result(check, err = null){    
  return {      
    check: check, //验证是否为空      
    err: err, //验证失败时的文案   
  } 
}

check(){
  for(let i = 0; i < this.vaild_item.length; i++){
    //循环每一个配置
    let vaild = this.vaild_item[i].rules.findIndex(function(val){
      //查找是否有isDefine规则
      return val === "isDefine";
    });
    if(vaild > -1){
      let rules_arr = this.vaild_item[i].rules;
      //如果存在isDefine规则,就把它去除,去处理剩下的规则
      rules_arr.splice(vaild,1);
      if(rules_arr.length > 0){
        for(let j = 0; j < rules_arr.length; j++){
          for(let o in this.rules()){
            if(rules_arr[j]['name'] == o && !this.rules()[rules_arr[j]['name']].call(this,this.vaild_item[i])){
              //将传入的配置规则和类中自带的规则进行匹配,如果匹配上并且匹配失败,返回验证信息 
              this.vaild = [this.check_result(false,this.error()[rules_arr[j]['name']].call(this,this.vaild_item[i]))];
              this.isCheck = false;
              return this.vaild;
            }else{
              this.isCheck = true;
              this.vaild = [this.check_result(true)];
            }
          }
        }
      }
    }else{
      if(this.vaild_item[i].rules.length > 0 && !!this.vaild_item[i].value){
        let rules_arr = this.vaild_item[i].rules;
        for(let j = 0; j < rules_arr.length; j++){
          //这部分处理逻辑和上面相同  
          for(let o in this.rules()){
            if(rules_arr[j]['name'] == o && !this.rules()[rules_arr[j]['name']].call(this,this.vaild_item[i])){
              this.vaild = [this.check_result(false,this.error()[rules_arr[j]['name']].call(this,this.vaild_item[i]))];
              this.isCheck = false;
              return this.vaild;
            }else{
              this.isCheck = true;
              this.vaild = [this.check_result(true)];
            }
          }
        }
      }
    }
  }
  return this.vaild;
}

check方法是核心逻辑,实际上就是上面流程图的内容,一些重要的地方我也备注了(实在抱歉,这部分代码写的有点儿丑,demo...demo...)。

最后我来展示一下这个工具用起来大概是什么样子的:

//配置文件
let vaild = [
  {
    value:this.username,
    type: "用户名",
    rules:[
      "isDefine", 
      {
        name:"limit",
        check: true,
        min:5,
        max:12
      }
    ]
  },
  {
    value:this.password,
    type: "密码",
    rules:[
      "isDefine", 
      {
        name:"mix",
        check:true,
      },
      {
        name:"limit",
        check: true,
        min:5,
        max:12
      }
    ]
  }
];

let vaildation = new Vaildation(); //新建一个Vaildation实例
vaildation.check_items = vaild;  //将配置传给实例中的check_items
let result = vaildation.check(); //ok,你已经拿到结果了

//噢,别忘了可以在result.check或者vaildation.isCheck中拿到是否成功的结果

这篇文章仅仅是我设计这个工具时的一个思路,可能还存在一堆问题,就先抛砖引玉一下,也希望大家能多多指正,最后感谢大家的收看。