1. 程式人生 > >優雅的表單驗證模式--策略設計模式和ES6的Proxy代理模式

優雅的表單驗證模式--策略設計模式和ES6的Proxy代理模式

轉載自 作者 @jawil 原文,原文有刪改

網站的互動,離不開表單的提交,而一個健壯的表單離不開對錶單內容的校驗。
假設我們正在編寫一個註冊的頁面,在點選註冊按鈕之前,有如下幾條校驗邏輯。

  1. 所有選項不能為空
  2. 使用者名稱長度不能少於6位
  3. 密碼長度不能少於6位
  4. 手機號碼必須符合格式
  5. 郵箱地址必須符合格式

一般情況下,我們都會選擇一種較為傳統的方式,對錶單內容進行校驗,假設表單結構如下:

<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繫結的函式的內部實現,這是違反了開放-封閉原則的。

  • 演算法的複用性差,如果程式中增加了另一個表單,這個表單也需要進行一些類似的校驗,那我們很可能將這些校驗邏輯複製得漫天遍野。

策略模式

策略模式的組成:

  1. 抽象策略角色:策略類,通常由一個介面或者抽象類實現。
  2. 具體策略角色:包裝了相關的演算法和行為。
  3. 環境角色:持有一個策略類的引用,最終給客戶端用的。
  • 具體策略角色——編寫策略類
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()
  })