前言
前段时间,老大搭好了 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 表单验证插件。下面介绍下思路。
一、表单验证模块的构成
任何表单验证模块都是由 配置——校验——报错——取值这几部分构成的。
下面是我老大针对公司项目给我提出的要求
我就很好奇地问, 为什么要这样子呢?然后老大就跟我一条一条解答:
我听了之后,大致懂了,原来之前自己写的 jq 表单验证还有这么多不舒服的点。-_-||| 接下来,是看看 vue 给我的好东西。让我来写
二、Vue 的插件怎么写
我一个 vue 小白,怎么就开始写 vue 插件了呢?那是因为想解决方案的时候,翻 Vue 文档翻到了这里
这些东东,等我写完 va.js 的时候,感觉尤大写的真的是很清楚了。
其实我是想写个指令来完成表单验证的事的。结果发现可能有 2-3 个指令,而且要再 Vue.prototype 上定义些方法,好让各个子实例内部也能拓展规则。于是老大说,这就相当于插件了。这让我很是吃鲸。
va.js 主要用的是 Vue 指令
Vue 文档真的写得很用心,但是我再补充一点吧
vnode.context 就是 Vue 的实例
我们做项目的时候,经常一个根组件上挂着 N 个子组件,子组件上又可能挂着 N 个子组件。vnode.context 获取的实例,是绑定该指令的组件的实例。这个就相当好用了。你可以做很多事情
当然还用了点 Vue.prototype Vue.prototype.$method 就是可以在各个组件上调用的方法。可以在组件内部用 this.$method 调用的
## 三、具体实现的思路 ##
核心思路如下图:
规则的构造函数
- //va配置的构造函数
- function VaConfig(type, typeVal, errMsg, name, tag) {
- this.type = type,
- this.typeVal = typeVal,
- this.errMsg = errMsg,
- this.name = name,
- this.tag = 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 新手的一次阶段性成果吧。哈哈~
使用实例
第一个框,加了两条指令
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 组件学习教程进行学习。
来源: http://www.jb51.net/article/110231.htm