网站的交互,离不开表单的提交,而一个健壮的表单离不开对表单内容的校验。
假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。
- 所有选项不能为空
- 用户名长度不能少于6位
- 密码长度不能少于6位
- 手机号码必须符合格式
- 邮箱地址必须符合格式
一般情况下,我们都会选择一种较为传统的方式,对表单内容进行校验,假设表单结构如下:
<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>
传统模式
对应的 JavaScript
校验规则如下:
// registerForm为所要提交表单的 id
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绑定的函数的内部实现,这是违反了开放-封闭原则的。算法的复用性差,如果程序中增加了另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野。
策略模式
策略模式的组成:
- 抽象策略角色:策略类,通常由一个接口或者抽象类实现。
- 具体策略角色:包装了相关的算法和行为。
- 环境角色:持有一个策略类的引用,最终给客户端用的。
- 具体策略角色——编写策略类
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
}
}
- 抽象策略角色——编写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(e) {
let errorMsg = validatorFunc()
if (errorMsg) {
// 注意,这里阻止表单提交,应该是通过阻止默认事件的方式,
// 而 `return false;` 或者 `return;` 都是没什么卵用的
e.preventDefault()
alert(errorMsg)
}
})
Proxy代理模式
- 利用proxy拦截不符合要求的数据:
let validator = (target, validator, errorMsg)=> {
return new Proxy(target, {
_validator: validator,
set(target, key, value, proxy) {
if(value === '') {
alert(`${errorMsg[key]}不能为空!`)
return target[key] = false
}
let va = this._validator[key]
if(!!va(value)) {
return Reflect.set(target, key, value, proxy)
} else {
alert(`${errorMsg[key]}格式不正确`)
return target[key] = false
}
}
})
}
- 负责校验的逻辑代码:
const validators = {
name(value) {
return value.length > 6
},
password(value) {
return value.length > 6
},
mobile(value) {
return /^1(3|5|7|8|9)[0-9]{9}$/.test(value)
},
email(value) {
return /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/.test(value)
}
}
- 客户端调用代码:
const errorMsg = { name: '用户名', password: '密码', mobile: '手机号码', email: '邮箱地址' }
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', (e)=>{
let validatorNext = function* (){
yield vali.name = registerForm.userName.value
yield vali.password = registerForm.passWord.value
yield vali.mobile = registerForm.phoneNumber.value
yield vali.email = registerForm.emailAddress.value
}
let validator = validatorNext()
validator.next()
let s = vali.name && validator.next() //上一步的校验通过才执行下一步
s = s ? vali.password && validator.next() : s
s = s ? vali.mobile && validator.next() : s
s = s ? vali.email && validator.next() : s
// 如果验证不通过,阻止表单提交
!s && e.preventDefault()
})