優雅的表單驗證模式--策略設計模式和ES6的Proxy代理模式
阿新 • • 發佈:2019-01-23
網站的互動,離不開表單的提交,而一個健壯的表單離不開對錶單內容的校驗。
假設我們正在編寫一個註冊的頁面,在點選註冊按鈕之前,有如下幾條校驗邏輯。
- 所有選項不能為空
- 使用者名稱長度不能少於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()
})