一次優雅的表單驗證設計


拋開不借助第三方庫不談,你在日常開發中是不是遇到過表單校驗的問題,比如姓名必須四中文,密碼必須是什么組合之內的。

我沒有,不你肯定有。

來來來,我們先看一段偽代碼:

// 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函數嘛,其實這個函數也是設計模式的一種原則開放-封閉(在不修改源碼的情況下,增加原函數的功能)。

好了,到這兒就再見了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM