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 相关文章推荐
JQuery在光标位置插入内容的实现代码
Jun 18 Javascript
JQuery 在文档中查找指定name的元素并移除的实现方法
May 19 Javascript
jQuery提示插件qTip2用法分析(支持ajax及多种样式)
Jun 08 Javascript
JavaScript简单获取页面图片原始尺寸的方法
Jun 21 Javascript
AngularJS 依赖注入详解和简单实例
Jul 28 Javascript
JS动态给对象添加属性和值的实现方法
Oct 21 Javascript
巧用canvas
Jan 21 Javascript
VUE前端cookie简单操作
Oct 17 Javascript
js数组去重的N种方法(小结)
Jun 07 Javascript
vue cli 3.0 搭建项目的图文教程
May 17 Javascript
JS中的算法与数据结构之链表(Linked-list)实例详解
Aug 20 Javascript
使用Vue实现一个树组件的示例
Nov 06 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
Drupal简体中文语言包安装教程
2014/09/27 PHP
Yii控制器中操作视图js的方法
2016/07/04 PHP
laravel框架关于搜索功能的实现
2018/03/15 PHP
php传值和传引用的区别点总结
2019/11/19 PHP
Jquery ajaxsubmit上传图片实现代码
2010/11/04 Javascript
关于jQuery的inArray 方法介绍
2011/10/08 Javascript
给jQuery方法添加回调函数一款插件的应用
2013/01/21 Javascript
封装好的js判断操作系统与浏览器代码分享
2015/01/09 Javascript
jQuery 获取跨域XML(RSS)数据的相关总结分析
2016/05/18 Javascript
jQuery实现查找链接文字替换属性的方法
2016/06/27 Javascript
结合代码图文讲解JavaScript中的作用域与作用域链
2016/07/05 Javascript
jquery tmpl模板(实例讲解)
2017/09/02 jQuery
JS中Attr的用法详解
2017/10/09 Javascript
JS实现div模块的截图并下载功能
2017/10/17 Javascript
Swiper自定义分页器使用详解
2017/12/28 Javascript
vue实现歌手列表字母排序下拉滚动条侧栏排序实时更新
2019/05/14 Javascript
JQuery常用选择器功能与用法实例分析
2019/12/23 jQuery
element-ui 实现响应式导航栏的示例代码
2020/05/08 Javascript
深入了解Vue.js 混入(mixins)
2020/07/23 Javascript
nodeJs项目在阿里云的简单部署
2020/11/27 NodeJs
python动态监控日志内容的示例
2014/02/16 Python
python中的闭包用法实例详解
2015/05/05 Python
Python代码解决RenderView窗口not found问题
2016/08/28 Python
python爬虫爬取某站上海租房图片
2018/02/04 Python
详解Python中的四种队列
2018/05/21 Python
python dataframe向下向上填充,fillna和ffill的方法
2018/11/28 Python
python使用递归的方式建立二叉树
2019/07/03 Python
Python画图实现同一结点多个柱状图的示例
2019/07/07 Python
wxpython多线程防假死与线程间传递消息实例详解
2019/12/13 Python
使用openCV去除文字中乱入的线条实例
2020/06/02 Python
Python 日期与时间转换的方法
2020/08/01 Python
美术师范毕业生自荐信
2013/11/16 职场文书
歌唱比赛获奖感言
2014/01/21 职场文书
初一学生评语大全
2014/04/24 职场文书
2015年团支部年度工作总结
2015/05/27 职场文书
JS中一些高效的魔法运算符总结
2021/05/06 Javascript