Vue表单验证插件的制作过程


Posted in Javascript onApril 01, 2017

前言

前段时间,老大搭好了Vue的开发环境,于是我们愉快地从JQ来到了Vue。这中间做的时候,在表单验证上做的不开心,看到vue的插件章节,感觉自己也能写一个,因此就自己开始写了一个表单验证插件va.js。 当然为什么不找个插件呢? vue-validator呀。

1.我想了下,一个是表单验证是个高度定制化的东西,这种网上找到的插件为了兼顾各个公司的需求,所以加了很多功能,这些我们不需要。事实证明,vue-validator有50kb,而我写的va.js只有8kb。
2.另一个是,vue-validator的api我真的觉得长, 动不动就v-validate:username="['required']",这么一长串,而我设计的调用大概如——v-va:Money

当然,本文仅是展示下,如何写个满足自己公司需求的vue表单验证插件。下面介绍下思路。

一、表单验证模块的构成

任何表单验证模块都是由 配置——校验——报错——取值 这几部分构成的。

  • 配置: 配置规则 和配置报错,以及优先级
  • 校验: 有在 change 事件校验, 在点击提交按钮的时候校验, 当然也有在input事件取值的
  • 报错: 报错方式一般要分,报错的文字有模板,也有自定义的
  • 取值: 将通过验证的数据返还给开发者调用

下面是我老大针对公司项目给我提出的要求

  • 集中式的管理 校验规则 和 报错模板。
  • 报错时机可选
  • 校验正确后的数据,已经打包成对象,可以直接用
  • 允许各个页面对规则进行覆盖,对报错信息进行自定义修改,以及允许ajax获取数据后,再对规则进行补充
  • 按顺序来校验,在第一个报错的框弹出错误

我就很好奇地问, 为什么要这样子呢?然后老大就跟我一条一条解答:

  • 集中式管理规则,和报错模板的好处,就是规则可以全局通用,一改全改。老大跟我说,光是昵称的正则就改了三次。如果这些正则写在各个页面,o( ̄ヘ ̄o#)哼,你就要改N个页面了
  • pc和移动的流程不一样,pc很多校验都要在change事件或者input事件就校验并报错了,而移动则一般是要到提交按钮再进行校验。所以写插件的时候要做好两手准备。然后,报错用的ui要可以支持我们现在用的layer插件。当然以后这个报错的ui也可能变,所以你懂滴。
  • 当然原来jq时代,我们的公用表单验证,就能验证完了,把数据都集合到一个对象里。这样ajax的时候,就不用再去取值了。你这个插件耶要达到这个效果
  • 原来jq的那个公用脚本,正则和报错都集中到一个地方去了,在很多地方已经很方便了。但是在一些页面需要改东西的时候还不够灵活。像RealName这个规则,最早是针对某个页面配置的,用的是后端接口上的字段名。另一个支付页,后端接口上的字段名改成了PayUser了,但是正则还是RealName的,原来我们是要复写一下RealName。这个就不太方便也不好看了。另外一个,支付金额,有最大值和最小值的限制,这个需要从后端获取的。你也要考虑这个情况。要做到各个页面上也能有一些灵活的地方可以修改规则,自定义报错等等。
  • 为什么要按顺序校验啊?你忘了上次牛哥让我们输入框,从上到下,按顺序报错。不然用户都不知道哪个地方错了。还有规则也是要按顺序的。哦哦哦。看来这次我放东西的时候,要用下数组了。尽量保持顺序。

我听了之后,大致懂了,原来之前自己写的jq表单验证还有这么多不舒服的点。-_-|||接下来,是看看vue给我的好东西。让我来写

二、Vue 的插件怎么写

我一个vue小白,怎么就开始写vue插件了呢?那是因为想解决方案的时候,翻Vue文档翻到了这里

Vue表单验证插件的制作过程

这些东东,等我写完va.js的时候,感觉尤大写的真的是很清楚了。

其实我是想写个指令来完成表单验证的事的。结果发现可能有2-3个指令,而且要再Vue.prototype上定义些方法,好让各个子实例内部也能拓展规则。于是老大说,这就相当于插件了。这让我很是吃鲸。

va.js主要用的是 Vue指令

Vue表单验证插件的制作过程

Vue表单验证插件的制作过程

Vue 文档真的写得很用心,但是我再补充一点吧

vnode.context 就是Vue的实例

我们做项目的时候,经常一个根组件上挂着N个子组件,子组件上又可能挂着N个子组件。vnode.context获取的实例,是绑定该指令的组件的实例。这个就相当好用了。你可以做很多事情

当然还用了点Vue.prototype
Vue.prototype.$method 就是可以在各个组件上调用的方法。可以在组件内部用 this.$method调用的

## 三、具体实现的思路 ##

核心思路如下图:

Vue表单验证插件的制作过程

规则的构造函数

//va配置的构造函数
function VaConfig(type, typeVal, errMsg, name, tag){
 this.type = type, this.typeVal = typeVal, this.errMsg = errMsg, this.name = name, this.tag = tag
}
  • type: nonvoid(非空), reg(正则), limit(区间), equal(与某个input相等),unique(不能相同)
  • typeVal: 根据不同type设置不同的值
  • errMsg: 自定义的报错信息
  • name: 用来传ajax的字段,如Password, Username
  • tag:用来报错的名字,如‘银行账号',‘姓名'

设置了三种规则

1.默认规则: 只要绑定指令,就默认有的校验。 比如非空的校验。 可以额外加修饰符来去除
2.选项规则: 通过Vue指令的修饰符添加的规则。
3.自定义规则: Vue指令属性值上添加的规则。
同一个type的规则只存在一个,也就是说,如果type为reg(正则),那么会互相覆盖。
覆盖的优先级: 自定义规则 > 选项规则 > 默认规则

思路讲的多了。也不知道怎么讲了,下面大家直接看源码把。

源码

/*
* 流程: 绑定指令-> 设置配置(vaConfig) -> 校验(check) -> 报错(showErr) 或 自定义报错
 */

var va = {};

function unique(arr){
 var hashTable = {}, newArr = [];
 for(var i = 0;i < arr.length;i++){
 if(!hashTable[arr[i]]){
  hashTable[arr[i]] = true;
  newArr.push(arr[i]);
 }
 }
 return newArr;
}

function addClass(dom, _class){
 var hasClass = !!dom.className.match(new RegExp('(\\s|^)' + _class + '(\\s|$)'))
 if(!hasClass){
 dom.className += ' ' + _class
 }
}

//校验函数
function check(v, conditions){
 var res = 0;     //0代表OK, 若为数组代表是某个字段的错误
 //验证函数
 var cfg = {
 //非空
 nonvoid: (v, bool)=>{
  if(bool){
  return v.trim() ? 0 : ['nonvoid'];
  }else{
  return 0;
  }
 },
 reg:(v, reg)=> reg.test(v) ? 0 : ['reg'],  //正则
 limit:(v, interval)=> (+v >= interval[0] && +v <= interval[1]) ? 0 : ['limit', interval],
 equal: (v, target)=>{       //和什么相等
  var _list = document.getElementsByName(target), _target
  for(var i = 0;i < _list.length;i++){
  if(_list[i].className.indexOf('va') > -1){
   _target = _list[i];
  }
  }
  return (_target.value === v) ? 0 : ['equal', _target.getAttribute('tag')]
 },
 unique:(v)=>{
  var _list = document.getElementsByClassName('unique'),
   valList = [].map.call(_list, item=>item.value)
  return (unique(valList).length === valList.length) ? 0 : ['unique']
 }
 }

 for(var i = 0;i < conditions.length;i++){
 var condi = conditions[i],
  type = condi.type,
  typeVal = condi.typeVal
 res = cfg[type](v, typeVal)
 // console.log(res, v, type,typeVal)
 //如果有自定义报错信息, 返回自定义的报错信息
 console.log(res)
 if(res){
  res = condi.errMsg || res
  break
 }
 }

 return res;
}

function showErr(name, checkResult){
 var type = checkResult[0],
  ext = checkResult[1] || []

 var ERR_MSG = {
 nonvoid: `${name}不能为空`,
 reg: `${name}格式错误`,
 limit: `${name}必须在${ext[0]}与${ext[1]}之间`,
 equal: `两次${ext}不相同`,
 unique: `${name}重复`
 }
 //使用layer来报错,如果需要自定义报错方式,要把全文的layer集中起来包一层。
 layer.msgWarn(ERR_MSG[type])
}

/**
 * [VaConfig va配置的构造函数]
 * @param {[string]} type [校验类型,如reg, limit等等]
 * @param {[*]} typeVal  [根据校验类型配置的值]
 * @param {[string]} errMsg [报错信息]
 * @param {[string]} name [用以ajax的字段名]
 * @param {[string]} tag [中文名,用以报错]
 */
function VaConfig(type, typeVal, errMsg, name, tag){
 this.type = type, this.typeVal = typeVal, this.errMsg = errMsg, this.name = name, this.tag = tag
}
//用来剔除重复的规则,以及规则的覆盖。默认后面的取代前面
Array.prototype.uConcat = function(arr){
 var comb = this.concat(arr)
  ,unique = {}
  ,result = []

 for(var i = 0;i < comb.length;i++){
 // console.log(i, comb[i])
 var type = comb[i].type
 if(unique[type]){
  var index = unique[type].index
  unique[type] = comb[i]
  unique[type].index = index;
 }else{
  unique[type] = comb[i]
  unique[type].index = i;
 }
 }

 for(var i= 0;i < 100;i++){
 for(var item in unique){
  if(unique[item].index === i){
  delete unique[item].index
  result.push(unique[item])
  }
 }
 }
 return result
}

//正则表
var regList = {
 ImgCode: /^[0-9a-zA-Z]{4}$/,
 SmsCode: /^\d{4}$/,
 MailCode: /^\d{4}$/,
 UserName: /^[\w|\d]{4,16}$/,
 Password: /^[\w!@#$%^&*.]{6,16}$/,
 Mobile: /^1[3|5|8]\d{9}$/,
 RealName: /^[\u4e00-\u9fa5 ]{2,10}$/,
 BankNum: /^\d{10,19}$/,
 Money: /^([1-9]\d*|0)$/,
 Answer: /^\S+$/,
 Mail: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/
}

va.install = function(Vue, options){
 Vue.directive('va',{
 bind:function(el, binding, vnode){
  var vm = vnode.context
  ,name = binding.arg === 'EXTEND' ? el.getAttribute('name') : binding.arg
  ,tag = el.getAttribute('tag')
  ,baseCfg = []     //默认的校验规则    --不用写,默认存在的规则(如非空)
  ,optionalConfig = []    //用户选择的配置成套  --与name相关
  ,customConfig = []     //用户自定义的规则(组件中) --bingding.value
  ,option = binding.modifiers
  ,regMsg = el.getAttribute('regMsg') || ''

  var eazyNew = (type, typeVal) =>{return new VaConfig(type, typeVal, '', name, tag)}
  var regNew = (typeVal) =>{return new VaConfig('reg', typeVal, regMsg, name, tag)} //正则的新建

  el.className = 'va' + vm._uid
  el.name = name

  vm.vaConfig || (vm.vaConfig = {})
  var NON_VOID = eazyNew('nonvoid', true)

  //默认非空,如果加了canNull的修饰符就允许为空
  if(!option.canNull){
  baseCfg.push(NON_VOID)
  }

  //需要立即校验的框
  if(option.vanow){
  el.addEventListener('change', function(){
   vm.vaResult || (vm.vaResult = {})
   vm.vaVal || (vm.vaVal = {})
   var value = el.value,
    conditions = vm.vaConfig[name],
    para = el.getAttribute('va-para')  //传给回调的参数

   //如果允许为空的此时为空,不校验
   if(value === '' && option.canNull){
   vm.vaVal[name] = value
   return
   }

   vm.vaResult[name] = check(value, conditions);
   var _result = vm.vaResult[name]
   if(_result){
   //如果返回的是字符串,则为自定义报错; 如果是数组,则使用showErr 报错
   typeof _result === 'string' ? layer.msgWarn(_result) : showErr(conditions[0].tag, _result)
   el.value = vm.vaVal[name] = ''
   return
   }
   vm.vaVal[name] = value
   vm.$vanow(para)   //写在实例内部method的回调
  })
  }

  //不能重复的
  if(option.unique){
  optionalConfig.push(eazyNew('unique', name))
  }

  //如果有在正则表里
  var regOptions = Object.keys(option);
  for(var i = 0;i < regOptions.length;i++){
  var regOption = regOptions[i]
  if(regList[regOptions[i]]){
   optionalConfig.push(regNew(regList[regOption]))
  }
  }

  //如果regList里有name对应的,直接就加进optionalConfig
  if(regList[name]){
  optionalConfig.push(regNew(regList[name]))
  }

  //用户自定义的规则
  if(binding.value){
  customConfig = binding.value.map(item=>{
   let type = Object.keys(item)[0];
   if(type === 'reg'){
   return regNew(item[type])
   }else{
   if(type === 'unique'){
    addClass(el, 'unique')
   }
   return eazyNew(type, item[type])
   }
  })
  }

  //规则由 默认规则 + 修饰符规则 + 写在属性的自定义规则 + 用户直接加到vm.vaConfig里的规则 合并(后面的同type规则会覆盖前面的)
  vm.vaConfig[name] || (vm.vaConfig[name] = [])
  vm.vaConfig[name] = baseCfg.uConcat(optionalConfig).uConcat(customConfig).uConcat(vm.vaConfig[name])
 },
 })

 Vue.directive('va-check', {
 bind:function(el, binding, vnode){
  var vm = vnode.context
  el.addEventListener('click', function(){
  var domList = document.getElementsByClassName('va' + vm._uid);
  vm.vaResult || (vm.vaResult = {})
  vm.vaVal || (vm.vaVal = {})

  for(var i = 0;i < domList.length;i++){
   var dom = domList[i],
    name = dom.name,
    value = dom.value,
    conditions = vm.vaConfig[name]

   var _result = check(value, conditions)
   //如果返回不为0,则有报错
   if(_result){
   //如果返回的是字符串,则为自定义报错; 如果是数组,则使用showErr 报错
   typeof _result === 'string' ? layer.msgWarn(_result) : showErr(conditions[0].tag, _result)
   return
   }
   vm.vaVal[name] = value
  }
  //校验通过的回调
  vm.$vaSubmit()
  // layer.msgWarn('全部校验成功')
  console.log(vm.vaVal)
  })

 }
 })

 Vue.directive('va-test',{
 bind: function(el, binding, vnode){
  var vm = vnode.context
  el.addEventListener('click', function(){
  vm.vaResult || (vm.vaResult = {})
  vm.vaVal || (vm.vaVal = {})

  var dom = document.getElementsByName(binding.arg)[0],
   name = dom.name,
   value = dom.value,
   conditions = vm.vaConfig[name]

  var _result = check(value, conditions)
  //如果返回不为0,则有报错
 console.log(_result)
  if(_result){
   //如果返回的是字符串,则为自定义报错; 如果是数组,则使用showErr 报错
   typeof _result === 'string' ? layer.msgWarn(_result) : showErr(conditions[0].tag, _result)
   return
  }

  vm.vaVal[name] = value
  var callback = Object.keys(binding.modifiers)[0]
  vm[callback]()
  })
 }
 })


 /**
 ** 在实例的monuted周期使用 api设置自定义配置
 */
 Vue.prototype.VaConfig = VaConfig
}

module.exports = va

现在项目已经用起来了。当然表单验证这种是高度定制化的。纯粹分享个过程和思路。也算我这个vue新手的一次阶段性成果吧。哈哈~

使用实例

Vue表单验证插件的制作过程

第一个框,加了两条指令

1.v-va:Password 这个代表使用配置表中password对应的配置(包括非空和正则,默认规则),同时应用Password作为校验成功获取的 数据对象的key
2.tag为报错显示中此输入框的名字

第二个框,为确认框,也加了两个指令
1.v-va:checkPassword.Password = "[{'equal':'Password'}]"
一般v-va后面的第一个字段为数据对象的key,他和正则对应的名字有可能不同。
这个字段如果和配置表中的配置匹配,那么自然应用配置。
如果不匹配,就要自己在后面用.的方式加配置(选项规则)。像这里的Password。

最后面还有一个 属性值 "[{'equal':'Password'}]"(自定义规则)。
这个地方用了数组,即会按这个数组的配置来进行校验。
同时这个数组有顺序,顺序代表规则的优先级。
这个配置代表,这个框必须和上面那个Password的框值相等,否则报错。
另外确认框不加入最后的结果数据对象。

2.tag 用来作为报错信息的名字

校验触发按钮 上面有一个指令 v-va-check
1.用来触发校验
2.校验成功后,将数据对象存在实例的vaVal属性下

根据上面的实例

规则的优先级:
1.自定义规则 > 选项规则 > 默认规则
2.规则中的优先级依照数组顺序

另外,可以看到为了使用者方便,我在我们团队中事先做了一些约定,并可能会用到 v-va、v-va-check、tag等指令,占用了实例的两个属性名vaConfig、vaVal。这些约定和设置可以使使用者使用方便(通过配置控制校验时机, 校验成功后自然生成通过的数据对象,自定义报错信息等等)。但是也减少了这个插件的普适性。

此方案仅提供各位做思路参考。个人认为,表单验证是高度定制化的需求,尽量根据各个业务情况进行取舍。在我的方案中,并不像vue-validator一样做了脏校验。

本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。

关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 判断undefined的实现代码
Nov 26 Javascript
jQuery 常见开发使用技巧总结
Dec 26 Javascript
跨浏览器的 mouseenter mouseleave 以及 compareDocumentPosition的使用说明
May 04 Javascript
jQuery Validate验证框架经典大全
Sep 23 Javascript
javascript常用经典算法实例详解
Nov 25 Javascript
Node.js实现兼容IE789的文件上传进度条
Sep 02 Javascript
微信小程序 video组件详解
Oct 25 Javascript
基于JS实现网页中的选项卡(两种方法)
Jun 16 Javascript
vue mint-ui学习笔记之picker的使用
Oct 11 Javascript
每天学点Vue源码之vm.$mount挂载函数
Mar 11 Javascript
JavaScript中的各种宽高属性的实现
May 08 Javascript
解决vue打包报错Unexpected token: punc的问题
Oct 24 Javascript
JS中使用正则表达式g模式和非g模式的区别
Apr 01 #Javascript
如何选择jQuery版本 1.x? 2.x? 3.x?
Apr 01 #jQuery
微信小程序网络请求的封装与填坑之路
Apr 01 #Javascript
微信小程序 列表的上拉加载和下拉刷新的实现
Apr 01 #Javascript
Node.js之网络通讯模块实现浅析
Apr 01 #Javascript
vue-cli+webpack记事本项目创建
Apr 01 #Javascript
JS常见创建类的方法小结【工厂方式,构造器方式,原型方式,联合方式等】
Apr 01 #Javascript
You might like
《忧国的莫里亚蒂》先导宣传图与STAFF公开
2020/03/04 日漫
谷歌音乐搜索栏的提示功能php修正代码
2011/05/09 PHP
redis 队列操作的例子(php)
2012/04/12 PHP
php阻止页面后退的方法分享
2014/02/17 PHP
qq登录,新浪微博登录接口申请过程中遇到的问题
2014/07/22 PHP
PHP使用JpGraph绘制折线图操作示例【附源码下载】
2019/10/18 PHP
php+js实现的拖动滑块验证码验证表单操作示例【附源码下载】
2020/05/27 PHP
IE6、IE7中setAttribute不支持class/for/rowspan/colspan等属性
2011/08/28 Javascript
浅谈js中的闭包
2015/03/16 Javascript
使用CSS+JavaScript或纯js实现半透明遮罩效果的实例分享
2016/05/09 Javascript
javascript中获取class的简单实现
2016/07/12 Javascript
Javascript 引擎工作机制详解
2016/11/30 Javascript
vue2.0构建单页应用最佳实战
2017/04/01 Javascript
微信小程序选择图片和放大预览图片功能
2017/11/02 Javascript
vue中各选项及钩子函数执行顺序详解
2018/08/25 Javascript
使用Angular Cli如何创建Angular私有库详解
2019/01/30 Javascript
[01:57]2018DOTA2亚洲邀请赛赛前采访-iG
2018/04/03 DOTA
在Python中使用sort()方法进行排序的简单教程
2015/05/21 Python
python执行子进程实现进程间通信的方法
2015/06/02 Python
深入学习python的yield和generator
2016/03/10 Python
Python学习入门之区块链详解
2017/07/25 Python
Python用5行代码写一个自定义简单二维码
2018/10/21 Python
python使用Qt界面以及逻辑实现方法
2019/07/10 Python
200行python代码实现2048游戏
2019/07/17 Python
Django如何继承AbstractUser扩展字段
2020/11/27 Python
HTML5 移动页面自适应手机屏幕四类方法总结
2017/08/17 HTML / CSS
运动会入场解说词
2014/02/07 职场文书
2014年消防工作实施方案
2014/02/20 职场文书
出纳员岗位职责风险
2014/03/06 职场文书
建议书标准格式
2014/03/12 职场文书
乡镇保密工作责任书
2014/07/28 职场文书
客房领班岗位职责
2015/02/11 职场文书
检察院起诉意见书
2015/05/20 职场文书
教你漂亮打印Pandas DataFrames和Series
2021/05/29 Python
springboot利用redis、Redisson处理并发问题的操作
2021/06/18 Java/Android
css让页脚保持在底部位置的四种方案
2022/07/23 HTML / CSS