一次优雅的表单验证设计
抛开不借助第三方库不谈,你在日常开发中是不是遇到过表单校验的问题,比如姓名必须四中文,密码必须是什么组合之内的。
我没有,不你肯定有。
来来来,我们先看一段伪代码:
// form表单提交的时候要做的事情
var validata = function () { if (!userser) { console.log('username must require!'); return false; } else if (!password) { console.log('password must require!'); }
// 提交数据到后台
}
从这段代码我们可以看出几个不好的问题:
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方法,它本身是就只有发送数据的功能,通过我们的改造,他是不是可以做其他的事情了,这些额外的功能他之前是没有的,如果你你愿意可以一直加上去,比如这样子:
很奇怪这里为什么要加上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函数嘛,其实这个函数也是设计模式的一种原则开放-封闭(在不修改源码的情况下,增加原函数的功能)。
好了,到这儿就再见了。