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

前言

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

我想了下,一个是表单验证是个高度定制化的东西,这种网上找到的插件为了兼顾各个公司的需求,所以加了很多功能,这些我们不需要。事实证明,vue-validator有50kb,而我写的va.js只有8kb。

另一个是,vue-validator的api我真的觉得长, 动不动就v-validate:username="['required']",这么一长串,而我设计的调用大概如——v-va:Money

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

一、表单验证模块的构成

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

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

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

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

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

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

我听了之后,大致懂了,原来之前自己写的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调用的

三、具体实现的思路

核心思路如下图:

规则的构造函数

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

设置了三种规则

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

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

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/*
* 流程: 绑定指令-> 设置配置(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-vav-va-checktag等指令,占用了实例的两个属性名vaConfigvaVal。这些约定和设置可以使使用者使用方便(通过配置控制校验时机, 校验成功后自然生成通过的数据对象,自定义报错信息等等)。但是也减少了这个插件的普适性。

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