JS-订阅发布-优化表单校验,超简单

906 阅读5分钟

在验证表单有效性的时候,会有很多项需要校验,好的做法是每一个校验写一个方法,等用户点击提交的时候,再去分别调用每一个方法。

这样的做法只是比把所有的逻辑写在一个方法里好些,但是还不够好。

我们可以用订阅发布模式来做继续的优化。

我们来看一个业务场景:

用户点击提交的时候会调用校验逻辑。该校验的过程有三个要求,

  1. 用户名不能为空
  2. 密码不能小于6位
  3. 手机号要满足/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。校验条件不满足的时候,就返回对应的错误信息

  1. 校验用户名不为空
function(value, errMsg) {
    if (value === '') {
        return errMsg
    }
}
  1. 校验密码的长度不能小于6位,或者指定的位数
function(value, length, errMsg) {
    if (value.length < length) {
        return errMsg
    }
}
  1. 校验手机号码的格式是否正确
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。该校验器有一个属性,和两个方法:

  1. 属性cache,用来储存要调用的函数
  2. add,往cache里面添加我们要订阅的函数
  3. 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;
        }
    }
}

总结:

这篇文章到这里就结束了,有几个需要注意的点

  1. 用apply方法加数组作为参数的方式,来实现不同函数参数不统一的情况
  2. 将submit函数和具体的校验逻辑分开,解耦有利于代码的阅读、维护、拓展
  3. 先整体,后具体的思考思路,是值得我们反复训练的