在验证表单有效性的时候,会有很多项需要校验,好的做法是每一个校验写一个方法,等用户点击提交的时候,再去分别调用每一个方法。
这样的做法只是比把所有的逻辑写在一个方法里好些,但是还不够好。
我们可以用订阅发布模式来做继续的优化。
我们来看一个业务场景:
用户点击提交的时候会调用校验逻辑。该校验的过程有三个要求,
- 用户名不能为空
- 密码不能小于6位
- 手机号要满足
/1[345678]\d{9}/的正则表达式
<form action="#" id="registerForm">
<p>
<label for="userName">请输入用户名称:</label>
<input type="text" name="userName">
</p>
<p>
<label for="password">请输入密码:</label>
<input type="password" name="password">
</p>
<p>
<label for="phoneNumber">请输入手机号:</label>
<input type="text" name="phoneNumber">
</p>
<button type="submit">提交</button>
</form>
对此,我们可以写下三个校验函数:
这三个函数,分别传入用户填写value,和errMsg。校验条件不满足的时候,就返回对应的错误信息
- 校验用户名不为空
function(value, errMsg) {
if (value === '') {
return errMsg
}
}
- 校验密码的长度不能小于6位,或者指定的位数
function(value, length, errMsg) {
if (value.length < length) {
return errMsg
}
}
- 校验手机号码的格式是否正确
function(value, errMsg) {
if (!/^1[3|4|5|7|8][0-9]{9}$/.test(value)) {
return errMsg
}
}
我们将三个函数用一个对象组装起来,这样方便调用
var strategies = {
isNotEmpty: function(value, errMsg) {
if (value === '') {
return errMsg
}
},
minLength: function(value, length, errMsg) {
if (value.length < length) {
return errMsg
}
},
isMobile: function(value, errMsg) {
if (!/^1[3|4|5|7|8][0-9]{9}$/.test(value)) {
return errMsg
}
}
}
下面我们来给表单注册校验方法,并且填入校验逻辑
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function(ev) {
//阻止表单的自动提交
var e = ev || window.event;
e.preventDefault();
//校验用户名是否正确
var res = strategies['isNotEmpty'](registerForm.userName.value, '用户名不能为空')
console.log(res);
if (res) return;
//校验密码是否正确
res = strategies['minLength'](registerForm.password.value, 6, '密码长度不能小于6位');
console.log(res);
if (res) return;
//校验手机号码是否正确
strategies['isMobile'](registerForm.phoneNumber.value, '手机格式不正确');
console.log(res);
if (res) return;
console.log('参数没问题');
}
看起来貌似没啥问题。那具体的校验逻辑和表单submit代码之间,分开的话,降低它们之间的耦合性,会不会更好呢?
嗯,我们来用发布订阅模式来解决这个问题吧
首先我们新建一个校验器类,Validator。该校验器有一个属性,和两个方法:
- 属性cache,用来储存要调用的函数
- add,往cache里面添加我们要订阅的函数
- start,调用之后,将会遍历调用cache里面的函数
这里搭了一个架子,我们将add和start放在Validator的原型对象上面,作为对象的方法。这样在Validator实例化的时候,就能够调用到,并且方法能够通过this来访问cache属性。
function Validator() {
this.cache = [];
}
Validator.prototype.add = function() {
this.cache.push(function() {
//
})
}
Validator.prototype.start = function() {
for (var i = 0; i < this.cache.length; i++) {
//
}
}
再来一个validatorFunction ---- 使用Validator的函数
虽然Validator的逻辑还没有填写,但是不妨碍我们假设已经有了这样一个校验器,我们可以这样使用它:
var validateFunction = function() {
//假设,现在已经有了一个Validator
var validator = new Validator();
/**
* add函数需要传入三个参数
* 第一个参数:用来传递用户输入的value
* 第二个参数:用来传递使用函数的名字
* 第三个参数:用来传递验证不通过时的,报错信息
*/
validator.add(registerForm.userName, 'isNotEmpty', '用户名不能为空');
validator.add(registerForm.password, 'minLength:6', '用户名长度不能小于6位');
validator.add(registerForm.phoneNumber, 'isMobile', '手机格式不正确');
var res = validator.start();
return res;
}
这里我们特别注意校验密码的函数,它还需要额外传入一个限定的长度。这里我们的做法是,同第二个参数一起传进去。
并且校验密码的函数需要三个参数,这个是和其他两个函数不一样的地方。难道我们要使用if/else,通过第二个参数,来判断不同的函数,然后根据不同的函数来传入不同的参数吗?
不是的,这样做对添加新的校验函数非常的不友好,而且对比上面一个方法,除了降低了耦合程度,还额外增加了复杂度。
if/else是非常违背开闭原则的,可能我们每一次添加新的校验函数,都要去修改代码,这是非常不好的。
那我们要如何做才能使这三个函数的操作达到统一呢,这是我们需要的思考的地方。
我们看看这个validateFunction是如何被submit函数使用的
这里我们调用validatefunction函数,并且获取它的返回值。如果该返回值不是undefined,那么就是校验不通过;如果返回值是undefined,那就校验通过。
var res = validateFunction();
if (res) {
console.log(res);
return;
}
console.log('参数没问题');
关键点到了,Validator里面的逻辑是如何实现的呢?我们接着往下看
/**
* 第一个参数:用户填写的dom,dom.value表示用户填写的值
* 第二个参数:函数校验的名称,或者”名称:附加信息“
* 第三个参数:用来传递验证不通过时的,报错信息
*/
Validator.prototype.add = function(dom, rule, errMsg) {
this.cache.push(function() {
//arr有三种情况
var arr = rule.split(':');// ['minLength', 6], ['isNotEmpty']
//将数组的第一个值弹出去,这个值一定是校验函数的名称
//这样arr中剩下1个值,或者0个值
var strategy = arr.shift(); //minLength、isNotEmpty、isMobile
//将用户输入的value,填入arr中第0个位置
//这样arr中就有了2个值,或者1个值
arr.unshift(dom.value); // [dom.value]、[dom.value, 6]
//再将报错信息填入arr数组的最后一个位置
//这样arr中就有了三个值,或者2个值
arr.push(errMsg);// [dom.value, errMsg]、[dom.value, 6, errMsg]
/**
* 此处通过apply调用校验函数,
* 第一个参数表示校验函数的指向,如果有特别要求,这里要特别注意
* 第二个参数表示校验函数时的参数,以数组的形式传进去
* return 校验函数返回的值
*/
return strategies[strategy].apply(dom, arr);
})
}
Validator.prototype.start = function() {
//遍历调用cache中每一个函数
for (var i = 0; i < this.cache.length; i++) {
var fn = this.cache[i];
var res = fn();
//如果res不是undefined,说明校验没有通过,
//直接return,终止start函数
if (res) {
return res;
}
}
}
总结:
这篇文章到这里就结束了,有几个需要注意的点
- 用apply方法加数组作为参数的方式,来实现不同函数参数不统一的情况
- 将submit函数和具体的校验逻辑分开,解耦有利于代码的阅读、维护、拓展
- 先整体,后具体的思考思路,是值得我们反复训练的