原文收录在个人 GitHub博客 (https://github.com/jawil/blog) ,喜欢的能够关注最新动态,你们一块儿多交流学习,共同进步,以学习者的身份写博客,记录点滴。html
在一个
Web
项目中,注册,登陆,修改用户信息,下订单等功能的实现都离不开提交表单。这篇文章就阐述了如何编写相对看着舒服的表单验证代码。前端
假设咱们正在编写一个注册的页面,在点击注册按钮以前,有以下几条校验逻辑。git
全部选项不能为空es6
用户名长度不能少于6位github
密码长度不能少于6位算法
手机号码必须符合格式编程
邮箱地址必须符合格式设计模式
注:为简单起见,如下例子以传统的浏览器表单验证,Ajax
异步请求不作探讨,浏览器端验证原理图:数组
简要说明:浏览器
这里咱们前端只作浏览器端的校验。不少工具能够在表单检验事后、浏览器发送请求前截取表单数据,攻击者能够修改请求中的数据,从而绕过
JavaScript
,将恶意数据注入服务器,这样会增长XSS(全称 Cross Site Scripting)攻击的机率。对于通常的网站,都不同意采用浏览器端的表单验证方法。浏览器端和服务器端双重验证方法在浏览器端验证方法基础上增长服务器端的验证,其原理如图所示,该方法增长服务器端的验证,弥补了传统浏览器端验证的缺点。若表单输入不符合要求,浏览器端的Javascript
验证能很快地给出响应,而服务器端的验证则能够防止恶意用户绕过Javascript
验证,保证最终数据的准确性。
HTML代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>探索几种表单验证最佳实践方式</title> </head> <body> <form action="http://xxx.com/register" id="registerForm" method="post"> <div class="form-group"> <label for="user">请输入用户名:</label> <input type="text" class="form-control" id="user" name="userName"> </div> <div class="form-group"> <label for="pwd">请输入密码:</label> <input type="password" class="form-control" id="pwd" name="passWord"> </div> <div class="form-group"> <label for="phone">请输入手机号码:</label> <input type="tel" class="form-control" id="phone" name="phoneNumber"> </div> <div class="form-group"> <label for="email">请输入邮箱:</label> <input type="text" class="form-control" id="email" name="emailAddress"> </div> <button type="button" class="btn btn-default">Submit</button> </form> </body> </html>
JavaScript代码:
let registerForm = document.querySelector('#registerForm') registerForm.addEventListener('submit', function() { if (registerForm.userName.value === '') { alert('用户名不能为空!') return false } if (registerForm.userName.length < 6) { alert('用户名长度不能少于6位!') return false } if (registerForm.passWord.value === '') { alert('密码不能为空!') return false } if (registerForm.passWord.value.length < 6) { alert('密码长度不能少于6位!') return false } if (registerForm.phoneNumber.value === '') { alert('手机号码不能为空!') return false } if (!/^1(3|5|7|8|9)[0-9]{9}$/.test(registerForm.phoneNumber.value)) { alert('手机号码格式不正确!') return false } if (registerForm.emailAddress.value === '') { alert('邮箱地址不能为空!') return false } if (!/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* $/.test(registerForm.emailAddress.value)) { alert('邮箱地址格式不正确!') return false } }, false)
这样编写代码,的确可以完成业务的需求,可以完成表单的验证,可是存在不少问题,好比:
registerForm.addEventListener
绑定的函数比较庞大,包含了不少的if-else
语句,看着都恶心,这些语句须要覆盖全部的校验规则。
registerForm.addEventListener
绑定的函数缺少弹性,若是增长了一种新的校验规则,或者想要把密码的长度校验从6改为8,咱们都必须深刻registerForm.addEventListener
绑定的函数的内部实现,这是违反了开放-封闭原则的。算法的复用性差,若是程序中增长了另外一个表单,这个表单也须要进行一些相似的校验,那咱们极可能将这些校验逻辑复制得漫天遍野。
所谓办法总比问题多,办法是有的,好比立刻要讲解的使用 策略模式 使表单验证更优雅更完美,我相信不少人很抵触设计模式,一听设计模式就以为很遥远,以为本身在工做中不多用到设计模式,那么你就错了,特别是JavaScript
这种灵活的语言,有的时候你已经在你的代码中使用了设计模式,只是你不知道而已。更多关于设计模式的东西,之后会陆续写博客描述,这里只但愿你们抛弃设计模式神秘的感受,通俗的讲,它无非就是完成一件事情通用的办法而已。
回到正题,假如咱们不想使用过多的 if - else
语句,那么咱们心中比较理想的代码编写方式是什么呢?咱们能不能像编写配置同样的去作表单验证呢?再来一个”一键验证“的功能,是否是很爽?答案是确定的,因此咱们心中理想的编写代码的方式以下:
// 获取表单form元素 let registerForm = document.querySelector('#registerForm') // 建立表单校验实例 let validator = new Validator(); // 编写校验配置 validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空') validator.add(registerForm.userName, 'minLength:6', '用户名长度不能小于6') // 开始校验,并接收错误信息 let errorMsg = validator.start() // 若是有错误信息输出,说明校验未经过 if(errorMsg){ alert(errorMsg) return false//阻止表单提交 }
怎么样?感觉感觉,是否是看上去优雅多了?好了,有了这些思路,咱们就能够向目标迈进了,下一步就要了解了解什么事策略模式了。
策略模式,单纯的看它的名字”策略“,指的是作事情的方法,好比咱们想到某个地方旅游,你能够有几种策略供选择:
一、飞机,嗖嗖嗖直接就到了,节省时间。
二、火车,能够选择高铁出行,专为飞机恐惧症者提供。
三、徒步,不失为一个锻炼身体的选择。
四、other method……
在程序设计中,咱们也常常遇到相似的状况,要实现一种方案有多种方案能够选择,好比,一个压缩文件的程序,便可选择zip算法,也能够选择gzip算法。
因此,作一件事你会有不少方法,也就是所谓的策略,而咱们今天要讲的策略模式也就是这个意思,它的核心思想是,将作什么和谁去作相分离。因此,一个完整的策略模式要有两个类,一个是策略类,一个是环境类(主要类),环境类接收请求,但不处理请求,它会把请求委托给策略类,让策略类去处理,而策略类的扩展是很容易的,这样,使得咱们的代码易于扩展。
在表单验证的例子中,各类验证的方法组成了策略类,好比:判断是否为空的方法(如:isNonEmpty),判断最小长度的方法(如:minLength),判断是否为手机号的方法(isMoblie)等等,他们组成了策略类,供给环境类去委托请求。下面,咱们就来实战一下。
策略模式的组成
抽象策略角色:策略类,一般由一个接口或者抽象类实现。
具体策略角色:包装了相关的算法和行为。
环境角色:持有一个策略类的引用,最终给客户端用的。
策略类很简单,它是由一组验证方法组成的对象,即策略对象,重构表单校验的代码,很显然第一步咱们要把这些校验逻辑都封装成策略对象:
/*策略对象*/ const strategies = { isNonEmpty(value, errorMsg) { return value === '' ? errorMsg : void 0 }, minLength(value, length, errorMsg) { return value.length < length ? errorMsg : void 0 }, isMoblie(value, errorMsg) { return !/^1(3|5|7|8|9)[0-9]{9}$/.test(value) ? errorMsg : void 0 }, isEmail(value, errorMsg) { return !/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) ? errorMsg : void 0 } }
根据咱们的思考,咱们使用add
方法添加验证配置,以下:
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空') validator.add(registerForm.userName, 'minLength:6', '用户名长度不能小于6')
add
方法接受三个参数,第一个参数是表单字段,第二个参数是策略对象中策略方法的名字,第三个参数是验证未经过的错误信息。
而后使用 start
方法开始验证,若验证未经过,返回验证错误信息,以下:
let errorMsg = validator.start()
另外,再解释一下下面这句代码:
add
方法第一个参数咱们说过了,是要验证的表单元素,第二个参数是一个字符串,使用 冒号(:) 分割,前面是策略方法名称,后面是传给这个方法的参数,第三个参数仍然是错误信息。
可是这种参数配置仍是有问题,咱们的要求是多种校验规则,好比用户名既不能为空,又要知足用户名长度不小于6,并非单一的,上面的为何要写两次,这种看着就不舒服,这时候我就须要对配置参数作一点小小的改动,咱们用数组来传递多个校验规则:
validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用户名不能为空!' }, { strategy: 'minLength:6', errorMsg: '用户名长度不能小于6位!' }])
最后是Validator类的实现:
/*Validator类*/ class Validator { constructor() { this.cache = [] //保存校验规则 } add(dom, rules) { for (let rule of rules) { let strategyAry = rule.strategy.split(':') //例如['minLength',6] let errorMsg = rule.errorMsg //'用户名不能为空' this.cache.push(() => { let strategy = strategyAry.shift() //用户挑选的strategy strategyAry.unshift(dom.value) //把input的value添加进参数列表 strategyAry.push(errorMsg) //把errorMsg添加进参数列表,[dom.value,6,errorMsg] return strategies[strategy].apply(dom, strategyAry) }) } } start() { for (let validatorFunc of this.cache) { let errorMsg = validatorFunc()//开始校验,并取得校验后的返回信息 if (errorMsg) {//r若是有确切返回值,说明校验没有经过 return errorMsg } } } }
使用策略模式重构代码之后,咱们仅仅经过‘配置’的方式就能够完成一个表单的校验,这些校验规则也能够复用在程序的任何地方,还能做为插件的形式,方便地被移植到其余项目中。
/*客户端调用代码*/ let registerForm = document.querySelector('#registerForm') const validatorFunc = () => { let validator = new Validator() validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用户名不能为空!' }, { strategy: 'minLength:6', errorMsg: '用户名长度不能小于6位!' }]) validator.add(registerForm.passWord, [{ strategy: 'isNonEmpty', errorMsg: '密码不能为空!' }, { strategy: 'minLength:', errorMsg: '密码长度不能小于6位!' }]) validator.add(registerForm.phoneNumber, [{ strategy: 'isNonEmpty', errorMsg: '手机号码不能为空!' }, { strategy: 'isMoblie', errorMsg: '手机号码格式不正确!' }]) validator.add(registerForm.emailAddress, [{ strategy: 'isNonEmpty', errorMsg: '邮箱地址不能为空!' }, { strategy: 'isEmail', errorMsg: '邮箱地址格式不正确!' }]) let errorMsg = validator.start() return errorMsg } registerForm.addEventListener('submit', function() { let errorMsg = validatorFunc() if (errorMsg) { alert(errorMsg) return false } }, false)
在修改某个校验规则的时候,只须要编写或者改写少许的代码。好比咱们想要将用户名输入框的校验规则改为用户名不能少于4个字符。能够看到,这时候的修改是绝不费力的。代码以下:
validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用户名不能为空!' }, { strategy: 'minLength:4', errorMsg: '用户名长度不能小于4位!' }])
策略模式利用组合、委托和多态等技术思想,能够有效的避免多种条件选择语句;
策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它易于切换,易于理解,易于拓展;
策略模式中的算法也能够复用在系统的其它地方,从而避免了许多重复的复制黏贴的工做;
在策略模式利用组合和委托来让Context拥有执行算法的能力,这也是继承一种更轻便的替代方案。
固然,策略模式也有一些缺点,但掌握了策略模式,这些缺点并不严重。
编写难度加大,代码量变多了,这是最直观的一个缺点,也算不上缺点,毕竟不能彻底以代码多少来衡量优劣。
首先,使用策略模式会在程序中增长许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好。
其次,要使用策略模式,必须了解全部的strategy,必须了解各个strategy之间的不一样点,这样才能选择一个合适的strategy。好比,咱们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时strategy要向客户暴露它的全部实现,这是违反最少知识原则的。
策略模式使开发人员可以开发出由许多可替换的部分组成的软件,而且各个部分之间是弱链接的关系。
弱链接的特性使软件具备更强的可扩展性,易于维护;更重要的是,它大大提升了软件的可重用性。
策略模式当然可行,可是包装的有点多了,并且不便于书写,代码书写量增长了很多,也就是有必定门槛,那有没有更好的实现方式呢?咱们能不能经过一层代理,在设置属性时候就去拦截它呢?这就是今天要讲到的ES6的Proxy对象。
Proxy 用于修改某些操做的默认行为,等同于在语言层面作出修改,因此属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为“代理器”。
let obj = new Proxy({}, { get (target, key, receiver) { console.log(`getting ${key}!`) return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { console.log(`setting ${key}!`) return Reflect.set(target, key, value, receiver) } })
上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get
)和设置(set
)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj
,去读写它的属性,就会获得下面的结果。
obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用本身的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
let proxy = new Proxy(target, handler);
Proxy 对象的全部用法,都是上面这种形式,不一样的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
下面是另外一个拦截读取属性行为的例子。
var proxy = new Proxy({}, { get: function(target, property) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35
上面代码中,做为构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即若是没有Proxy
的介入,操做原来要访问的就是这个对象;第二个参数是一个配置对象,对于每个被代理的操做,须要提供一个对应的处理函数,该函数将拦截对应的操做。好比,上面代码中,配置对象有一个get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是目标对象和所要访问的属性。能够看到,因为拦截函数老是返回35
,因此访问任何属性都获得35
。
注意,要使得Proxy
起做用,必须针对Proxy
实例(上例是proxy
对象)进行操做,而不是针对目标对象(上例是空对象)进行操做。
利用proxy拦截不符合要求的数据
function validator(target, validator, errorMsg) { return new Proxy(target, { _validator: validator, set(target, key, value, proxy) { let errMsg = errorMsg if (value == '') { alert(`${errMsg[key]}不能为空!`) return target[key] = false } let va = this._validator[key] if (!!va(value)) { return Reflect.set(target, key, value, proxy) } else { alert(`${errMsg[key]}格式不正确`) return target[key] = false } } }) }
负责校验的逻辑代码
const validators = { name(value) { return value.length > 6 }, passwd(value) { return value.length > 6 }, moblie(value) { return /^1(3|5|7|8|9)[0-9]{9}$/.test(value) }, email(value) { return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) } }
客户端调用代码
const errorMsg = { name: '用户名', passwd: '密码', moblie: '手机号码', email: '邮箱地址' } const vali = validator({}, validators, errorMsg) let registerForm = document.querySelector('#registerForm') registerForm.addEventListener('submit', function() { let validatorNext = function*() { yield vali.name = registerForm.userName.value yield vali.passwd = registerForm.passWord.value yield vali.moblie = registerForm.phoneNumber.value yield vali.email = registerForm.emailAddress.value } let validator = validatorNext() validator.next(); !vali.name || validator.next(); //上一步的校验经过才执行下一步 !vali.passwd || validator.next(); !vali.moblie || validator.next(); }, false)
优势:条件和对象自己彻底隔离开,后续代码的维护,代码整洁度,以及代码健壮性和复用性变得很是强。
缺点:兼容性很差,有babel怕啥,粗糙版,不少细节其实还能够优化,这里只提供一种思路。