一次优雅的表单验证设计

抛开不借助第三方库不谈,你在日常开发中是不是遇到过表单校验的问题,比如姓名必须四中文,密码必须是什么组合之内的。

我没有,不你肯定有。

来来来,我们先看一段伪代码:

// form表单提交的时候要做的事情
var
validata = function () { if (!userser) { console.log('username must require!'); return false; } else if (!password) { console.log('password must require!'); }
  // 提交数据到后台
 } 
这个就是一个很基础的验证用户名和密码的验证,如果只有这两个需要校验,ok没有问题。但是往往我们的验证比较复杂,字段也比较多,如果你还是这样子写的话,对不起我只想说我要吐(其实我之前也是这么写的)。当时觉得没有什么,可是随着自己的成长,越看越像一坨屎(嗯,那个我说的是代码,并不是我自己)。

从这段代码我们可以看出几个不好的问题:

1.需要嵌套大量的if else语句,不太美观,而且暴露自己实力低下了。

2.表单提交函数做了很多事情,不管发送数据,还要负责表单的校验。我们编程讲究的就是单一原则,方便函数的复用,不添加额外的副作用。

可以,我们一个一个的来改造,我们现在先来改造第2个问题,(为什么不先改造第一个问题了,废话,是我在写不是你在写,开玩笑的,第一个我们后面慢慢解决)。

且看如下完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>优雅的表单校验</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .myForm {
            width: 300px;
            padding-bottom: 30px;
            border: 1px solid red;
            margin: 100px auto;
        }

        .form-component>div:first-child {
            margin: 10px 20px 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
            overflow: hidden;
        }

        input {
            outline: none;
            display: block;
            border: none;
            padding: 10px;
        }

        #submit {
            padding: 5px 20px;
            outline: none;
            border: 1px solid skyblue;
            background-color: skyblue;
            letter-spacing: 1em;
            border-radius: 5px;
            margin: 0 auto;
            display: block;
        }

        .errMsg {
            color: red;
            margin: 0 20px;
        }
    </style>
</head>

<body>
    <div id="root">
        <form class="myForm">
            <div class="form-component">
                <div><input type="text" class="username" placeholder="请输入用户名"></div>
                <div>
                    <p class="username-err"></p>
                </div>
            </div>
            <div class="form-component">
                <div><input type="text" class="password" placeholder="请输入密码"></div>
                <div>
                    <p class="password-err"></p>
                </div>
            </div>
            <button id="submit">提交</button>
        </form>
    </div>
    <script>
        var submit = document.getElementById('submit');
        var username = document.getElementsByClassName('username')[0];
        var password = document.getElementsByClassName('password')[0];
        var usernameErrText = document.getElementsByClassName('username-err')[0];
        var passworErrText = document.getElementsByClassName('password-err')[0];
        // 点击提交按钮,发送数据(前提是校验通过)
        // 校验规则,用户名和密码不为空即可
    </script>
</body>

</html>

现在我们的雏形搭建好了,现在就来做提交的事情:

// 点击提交按钮,发送数据(前提是校验通过)
        // 校验规则,用户名和密码不为空即可

        var validata = function () {
            usernameErrText.innerHTML = passworErrText.innerHTML = '';
            if (!username.value) {
                usernameErrText.innerHTML = '请填写用户名!';
                return false
            }
            if (!password.value) {
                passworErrText.innerHTML = '请填写密码!';
                return false;
            }
            return true;
        }
        var postData = function () {
            console.log('发送数据给后台');
        }
        submit.onclick = function (e) {
            e.preventDefault();
            if (validata()) {
                postData();
            }
        }

现在我们把验证数据和提交数据放到了一起操作,是不是感觉比之前好多了,如果有一天不需要验证了,那么我们就直接调用postData方法就行了。但是,骚年你真的就这样子满足了嘛,你信不信还有更骚的操作等着你,我们看看这段代码:

Function.prototype.before = function (fn) {
            var _that = this; // 保存原函数的引用
            return function () {
                fn.apply(this, arguments) && _that.apply(this, arguments)
            }
        }

        var validata = function () {
            usernameErrText.innerHTML = passworErrText.innerHTML = '';
            if (!username.value) {
                usernameErrText.innerHTML = '请填写用户名!';
                return false
            }
            if (!password.value) {
                passworErrText.innerHTML = '请填写密码!';
                return false;
            }
            return true;
        }
        var postData = function () {
            console.log('发送数据给后台');
        }
        var fomrSubmit = postData.before(validata);
        submit.onclick = function (e) {
            e.preventDefault();
            fomrSubmit();
        }

现在我们修改了一下代码,重点看一下这里:

Function.prototype.before = function (fn) {
            var _that = this; // 保存原函数的引用
            return function () {
                fn.apply(this, arguments) && _that.apply(this, arguments)
            }
        }

仔细看,仔细瞧,我们这里是不是强化了最初调用before的那个函数,在这里就是postData方法,它本身是就只有发送数据的功能,通过我们的改造,他是不是可以做其他的事情了,这些额外的功能他之前是没有的,如果你你愿意可以一直加上去,比如这样子:

var fomrSubmit = postData.before(validata).before(function(){
console.log(1);
return true;
});

很奇怪这里为什么要加上return true了?其实一点不奇怪,因为我们最初设计的就是要校验通过才能发送数据的,因为这个设置,所以我们之前执行的函数都需要返回true,才会继续往下执行。当然这个并不是重点,你愿意的话可以取消这个限制,就可以不用返回true也可以执行很多函数了。

当然,有些人是肯定不会喜欢这种修改原型的实现方式的,那我们就换一个方法:

 // Function.prototype.before = function (fn) {
        //     var _that = this; // 保存原函数的引用
        //     return function () {
        //         fn.apply(this, arguments) && _that.apply(this, arguments)
        //     }
        // }
        var before = function(fn1,fn2) {
            return function(){
                fn1.apply(this,arguments) &&
                fn2.apply(this,arguments);
            }
        }

        var validata = function () {
            usernameErrText.innerHTML = passworErrText.innerHTML = '';
            if (!username.value) {
                usernameErrText.innerHTML = '请填写用户名!';
                return false
            }
            if (!password.value) {
                passworErrText.innerHTML = '请填写密码!';
                return false;
            }
            return true;
        }
        var postData = function () {
            console.log('发送数据给后台');
        }
        // var fomrSubmit = postData.before(validata);
        var fomrSubmit = before(validata,postData);
        submit.onclick = function (e) {
            e.preventDefault();
            fomrSubmit();
        }

这里我们定义了before方法,也同样能实现之前的功能。before方法定义的与否只是看自己的习惯而已,我这里还是采用原型上定义方法在继续下面的讲解。

看到这里,聪明的你肯定会说,我点击提交按钮的时候,分别去执行这两个函数不就行了嘛?对啊,我们搞这么半天不就是为了实现这个功能嘛,只是代码写的不一样。

我相信更聪明的你想到了,我们这个实现方式就是AOP,也是装饰者模式在JavaScript中的一种体现。或许现在你觉得这个不重要,但你维护别人的代码或者使用第三方库的时候,在不修改别人源代码的时候(别人的代码或许是混合压缩过的),增强原函数的功能,你就知道这个模式有什么用处了。好了这部份的优化我们就到这里。

接下来我们看一下,关于校验的问题,就像我之前所说的那样如果校验字段很少或者要校验的东西很少(比如用户名判断是否为空,不判断长度)你写if else没有错,但是事与愿违,一般涉及到表单校验是要校验很多的东西。如果你还是if else嵌套下去,我相信你自己都看不下去自己写的代码。

我们先来看一段代码:

 <script>
        var submit = document.getElementById('submit');
        var username = document.getElementsByClassName('username')[0];
        var password = document.getElementsByClassName('password')[0];
        var usernameErrText = document.getElementsByClassName('username-err')[0];
        var passworErrText = document.getElementsByClassName('password-err')[0];
        // 点击提交按钮,发送数据(前提是校验通过)
        // 校验规则,用户名和密码不为空即可

        Function.prototype.before = function (fn) {
            var _that = this; // 保存原函数的引用
            return function () {
                fn.apply(this, arguments) === 0 && _that.apply(this, arguments)
            }
        }
        var postData = function () {
            console.log('发送数据给后台');
        }

        // 定义表单校验对象
        var validataor = (function () {
            // 定义校验规则
            var rules = {
                isNotEmpty: function (dom, errMsg) {
                    if (!dom.value.trim()) {
                        return errMsg;
                    }
                },
            }
            // 需要校验的函数集合
            var caches = [];
            // 错误数量
            var errNum = 0;
            return {
                start: function () {
                    for (var i = 0, func; func = caches[i++];) {
                        if (func()) {
                            errNum++;
                        }
                    }
                },
                add: function (dom, rule, errMsg, errShowDom) {
                    caches.push(function () {
                        var msg = rules[rule](dom, errMsg);
                        msg && (errShowDom.innerHTML = msg) || (errShowDom.innerHTML = '');
                        return msg;
                    });

                },
                isCheckAll: function () {
                    var num = errNum;
                    errNum = 0;
                    caches.length = 0;
                    return num;
                }
            }
        })();


        var fomrSubmit = postData.before(function () {
            validataor.add(username, 'isNotEmpty', '用户名必填', usernameErrText);
            validataor.add(password, 'isNotEmpty', '密码必填', passworErrText);
            validataor.start();
            return validataor.isCheckAll()
        });

        submit.onclick = function (e) {
            e.preventDefault();
            fomrSubmit();
        }

    </script>

这段代码,我们也实现了验证功能,咋一看我擦,怎么这么复杂。没错封装代码就是有可能带来额外的代码,但是你看一下我们还有if else那种难看的嵌套嘛。

这里我们用validataor对象,实现校验规则的新增,检测。

而且这样子写我们还可以随意的配置自己的校验规则:

 // 定义表单校验对象
        var validataor = (function (rules) {
            // 需要校验的函数集合
            var caches = [];
            // 错误数量
            var errNum = 0;
            return {
                start: function () {
                    for (var i = 0, func; func = caches[i++];) {
                        if (func()) {
                            errNum++;
                        }
                    }
                },
                add: function (dom, rule, errMsg, errShowDom) {
                    caches.push(function () {
                        var msg = rules[rule](dom, errMsg);
                        msg && (errShowDom.innerHTML = msg) || (errShowDom.innerHTML = '');
                        return msg;
                    });

                },
                isCheckAll: function () {
                    var num = errNum;
                    errNum = 0;
                    caches.length = 0;
                    return num;
                }
            }
        })({
            isNotEmpty: function (dom, errMsg) {
                if (!dom.value.trim()) {
                    return errMsg;
                }
            },
            isPhone: function(){
                // 校验是否是手机号码
            }
        });

看现在我们可以随意配置,你只要知道这个validataor的用法,岂不是很简单,不需要在一个一个if else的去判断,多优雅。而且我们这个validataor对象很方便移植。

聪明的你应该看出了隐藏在代码中的策略模式的使用,这里我就不指出,免得班门弄斧了。

好了改造我们现在差不多了,我们现在需要升级。实际中不可能每一个字段都只有一种校验,有的有着多个校验。我们拿密码距离,密码不能为空而且长度不能小于6位。

现在我们有一个最坏原则,那就是表单校验的其中一个字段有多个校验,我们假设它在校验的时候遇到第一个校验不通过的情景,就停止后面的校验(本来也是这样子),

那么什么是最坏原则了,那就是我们所期望校验的时候,总会存在校验不通过的情况,如果连最坏原则都通过了,那就说明你的所有校验都通过。

我们先来看一段代码:

var Chain = function (fn) {
    this.fn = fn;
    this.nextChain = null;
}
Chain.prototype.setNextChain = function (nextChain) {
    this.nextChain = nextChain;
}
Chain.prototype.next = function () {
    var ret = this.fn.apply(this, arguments);
    if (ret === 'next') {

        return this.nextChain && this.nextChain.next.apply(this.nextChain, arguments);
    }
    return ret;
}

var fn1 = function (value) {
    if (value < 10 && value > 5) {
        console.log('fn1满足');
    }
    else {
        return 'next';
    }

}
var fn2 = function (value) {
    console.log(value);
    if (value > 10 && value < 20) {
        console.log('fn2满足');
    }
    else {
        return 'next';
    }

}
var fn3 = function (value) {
    if (value > 30) {
        console.log('fn3满足');
    }
    else {
        return 'next';
    }

}

var chainF1 = new Chain(fn1);
var chainF2 = new Chain(fn2);
var chainF3 = new Chain(fn3);

chainF1.setNextChain(chainF2);
chainF2.setNextChain(chainF3);

chainF1.next(8);

这段代码,我们测试所给数字的大小范围,通过Chain类的各个实例,我们完全摒弃了以前的if else的嵌套,是不是很优雅。每一个执行函数,如果满足它的要求,就会停止所有的程序执行,如果不满足,那么就把执行权交给下一个chain实例中的执行函数。如果最后的结果返回的不是next那么就代表所有的校验都通过了。

我们现在在优化一下这段代码:

var fn1 = function (value) {
    if (value < 10 && value > 5) {
        console.log('fn1满足');
    }
    else {
        return 'next';
    }

}
var fn2 = function (value) {
    if (value > 10 && value < 20) {
        console.log('fn2满足');
    }
    else {
        return 'next';
    }

}
var fn3 = function (value) {
    if (value > 30) {
        console.log('fn3满足');
    }
    else {
        return 'next';
    }

}


Function.prototype.after = function (fn) {
    var _that = this;
    return function () {
        var ret = _that.apply(this, arguments);
        if (ret === 'next') {
            return fn.apply(this, arguments);
        }
        return ret;
    }
}
var start = fn1.after(fn2).after(fn3);
start(18);

现在你来来看看这个威力,是不是很强大,聪明的你肯定知道这个after是AOP的实现。

好了,现在有了这段代码我们,就来实现我们的完整校验。

1.支持多字段校验

2.一个字段支持多种校验

3.校验一个出错,停止后面所有的校验

现在我就直接给出完整的代码,我相信你一定能看懂的(不是相信,是你一定能,因为你很棒啊):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>优雅的表单校验</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .myForm {
            width: 300px;
            padding-bottom: 30px;
            border: 1px solid red;
            margin: 100px auto;
        }

        .form-component>div:first-child {
            margin: 10px 20px 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
            overflow: hidden;
        }

        input {
            outline: none;
            display: block;
            border: none;
            padding: 10px;
        }

        #submit {
            padding: 5px 20px;
            outline: none;
            border: 1px solid skyblue;
            background-color: skyblue;
            letter-spacing: 1em;
            border-radius: 5px;
            margin: 0 auto;
            display: block;
        }

        .errMsg {
            color: red;
            margin: 0 20px;
        }
    </style>
</head>

<body>
    <div id="root">
        <form class="myForm">
            <div class="form-component">
                <div><input type="text" class="username" placeholder="请输入用户名"></div>
                <div>
                    <p class="username-err errMsg"></p>
                </div>
            </div>
            <div class="form-component">
                <div><input type="password" class="password" placeholder="请输入密码"></div>
                <div>
                    <p class="password-err errMsg"></p>
                </div>
            </div>
            <button id="submit">提交</button>
        </form>
    </div>
    <script>
        var submit = document.getElementById('submit');
        var username = document.getElementsByClassName('username')[0];
        var password = document.getElementsByClassName('password')[0];
        var usernameErrText = document.getElementsByClassName('username-err')[0];
        var passworErrText = document.getElementsByClassName('password-err')[0];
        // 点击提交按钮,发送数据(前提是校验通过)
        // 校验规则,用户名和密码不为空即可

        Function.prototype.before = function (fn) {
            var _that = this; // 保存原函数的引用
            return function () {
                fn.apply(this, arguments) === 0 && _that.apply(this, arguments)
            }
        }
        Function.prototype.after = function (fn) {
            var _that = this;
            return function () {
                var ret = _that.apply(this, arguments);
                // 最坏原则,这次校验通过,假设后面有校验不会通过
                if (typeof ret === 'undefined') {
                    return fn.apply(this, arguments);
                }
                // 如果这次校验不通过,那么停止校验,返回错误信息
                return ret;
            }
        }
       
        var validataor = (function (validataRules) {
            var caches = [];
            var errNum = 0;
            return {
                add: function (dom, rules, errShowDom) {
                    var fnsArr = [];
                    for (var i = 0, ruleObj; ruleObj = rules[i++];) {
                        var ruleArr = ruleObj.rule.split(':');
                        var rule = ruleArr.shift();
                        ruleArr.unshift(dom);
                        ruleArr.push(ruleObj.errMsg);
                        fnsArr.push(validataRules[rule].bind(dom, ...ruleArr));
                    }
                    if (fnsArr.length) {
                        var fn = fnsArr.shift();
                        while (fnsArr.length) {
                            fn = fn.after(fnsArr.shift());
                        }
                        caches.push({
                            fn: fn,
                            container: errShowDom
                        });
                    }

                },
                start: function () {
                    for (var i = 0, cacheObj; cacheObj = caches[i++];) {
                        var msg = cacheObj.fn();
                        cacheObj.container.innerHTML = msg || '';
                        imsg && ++errNum;
                    }
                    caches = [];
                    var num = errNum;
                    errNum = 0;
                    return num;
                }
            }
        })({
            isNotEmpty: function (dom, errMsg) {
                if (!dom.value) {
                    return errMsg;
                }
            },
            isPhone: function () {
                // 校验是否是手机号码
            },
            minlength: function (dom, length, errMsg) {
                if (dom.value.length < length) {
                    return errMsg;
                }
            }
        });

        var postData = function () {
            console.log('发送数据给后台');
        }
        var fomrSubmit = postData.before(function () {
            validataor.add(username, [
                { rule: 'isNotEmpty', errMsg: '用户名必填' }
            ], usernameErrText);
            validataor.add(password, [
                { rule: 'isNotEmpty', errMsg: '密码必填' },
                { rule: 'minlength:10', errMsg: '密码长度必须大于等于10位' },

            ], passworErrText);
            return validataor.start();
        });

        submit.onclick = function (e) {
            e.preventDefault();
            fomrSubmit();
        }

    </script>
</body>

</html>

现在我们只需要自己配置好校验规则,就可以实现不同字段的校验,当然本案例代码肯定只有优化的地方,我现在是写最多的代码,希望理解的够清楚一些。

相比之前的if else现在我个人感觉是好多了。但是我们发现了代码量增多了,多的就是validatator这一段代码,这段代码其实不难。牛逼的你应该能从本案例中发现许多设计模式的运用(策略模式,职责链模式,装饰者模式),还有AOP的风格的编程,你看见确实是增加了代码量,所以实际项目还是要看看引入设计模式会不会得不偿失。

反正在这个例子中,我认为是没有错的,只需要自己配置就行,代码中不变的地方已经被我们封装起来了,变化的地方我们提出来了,其实这就是设计模式通用的手段,还记得before函数嘛,其实这个函数也是设计模式的一种原则开放-封闭(在不修改源码的情况下,增加原函数的功能)。

好了,到这儿就再见了。

 

 

posted @ 2020-03-26 22:08  只会一点前端  阅读(974)  评论(0编辑  收藏  举报