优雅的表单验证模式--策略设计模式和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()
  })
阅读更多
换一批

没有更多推荐了,返回首页