Skip to content

表单校验

在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。

  • 用户名不能为空。
  • 密码长度不能少于 6 位。
  • 手机号码必须符合格式。
html
<html>
  <body>
    <form action="http:// xxx.com/register" id="registerForm" method="post">
      请输入用户名:<input type="text" name="userName"/ >
      请输入密码:<input type="text" name="password"/ > 
      请输入手机号码:<input type="text" name="phoneNumber"/ >
      <button>提交</button>
    </form>
  <script>
    const registerForm = document.getElementById('registerForm')
    
    registerForm.onsubmit = function () {
      if (registerForm.userName.value === ''){
        alert ('用户名不能为空')
        return false
      }

      if (registerForm.password.value.length < 6){
        alert ('密码长度不能少于 6 位')
        return false
      }

      if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)){
        alert ('手机号码格式不正确')
        return false
      }
    }
  </script>
  </body>
</html>

它的缺点跟计算奖金的最初版本一模一样。第一是registerForm.onsubmit 函数比较庞大,包含 了很多 if-else 语句,这些语句需要覆盖所有的校验规则。第二十registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6 改成 8,我们都必 须深入 registerForm.onsubmit 函数的内部实现,这是违反开放—封闭原则的。第三是算法的复 用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能 将这些校验逻辑复制得漫天遍野。

接下来使用策略模式重构表单验证代码,第一步显然是将校验逻辑封装成策略对象。

javascript
const strategies = {
  isNonEmpty (value, errorMsg) {
    if (value === '') {
      return errorMsg
    }
  },
  minLength (value, length, errorMsg) {
    if (value.length < length) {
      return errorMsg
    }
  },
  isMobile (value, errorMsg) {
    if (!/(^1[3|5|8][0-9]{9}$)/.test(value)){
      return errorMsg
    }
  }
}

第二步是实现 Validator 类,当用户提交表单后,通过 validateFunc 是否有确切返回 errorMsg,判断表单验证是否通过。validateFunc 中会创建 Validator 实例,可以通过 validator.add() 添加校验规则,添加完后会调用 validator.start()方法来启动校验。 如果 validator.start() 返回了一个确切的 errorMsg 字符串当作返回值,说明该次校验 没有通过,此时需要阻止表单的提交。

javascript
const Validator = function () {
  this.cache = [] // 添加的校验规则
}

/**
 * 添加校验规则
 * 
 * @param {*} dom - 参与校验的 DOM
 * @param {string} rule - 校验规则,是一个以冒号隔开的字符串。冒号前面的
 *  minLength 代表客户挑选的 strategy 对象,冒号后面的数字 6 表示在校验过程中所必需
 *  的一些参数。'minLength:6' 的意思就是校验 registerForm.password 这个文本输入框
 *  的 value 最小长度为 6。如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数
 *  信息,比如'isNonEmpty'。
 * @param {string} errorMsg - 未通过校验时的错误提示
 * @return {(string|undefined)} 未通过校验时返回 errorMsg,通过校验时返回 undefined
 */
Validator.prototype.add = function (dom, rule, errorMsg) {
  const ary = rule.split(':') // 获取对应策略和校验过程中的额外参数

  this.cache.push(function () {
    const strategy = ary.shift()
    ary.unshift(dom.value)
    ary.push(errorMsg)
    return strategies[strategy].apply(dom, ary)
  })
}

Validator.prototype.start = function () {
  const { cache } = this

  for (let i = 0, validateFunc; validateFunc = cache[i++];) {
    const errorMsg = validateFunc()
    
    if (errorMsg) {
      return errorMsg
    }
  }
}

const validateFunc = (function () {
  const validator = new Validator()

  // 添加校验规则
  validator.add(registerForm.username, 'isNonEmpty', '用户名不能为空')
  validator.add(registerForm.password, 'minLength:5', '密码长度至少为5')
  // ...

  return function () {
    const errorMsg = validator.start()

    return errorMsg
  }
})()

submitBtn.addEventListener('click', () => {
  const errorMsg = validateFunc()

  if (errorMsg) {
    // Fail, do something...

    return
  }

  // Success, do something...
})

给一个输入框同时添加多种校验规则。

javascript
validator.add(registerForm.username, [
  {
    strategy: 'isNonEmpty', 
    errorMsg: '用户名不能为空'
  },
  {
    strategy: 'minLength: 6', 
    errorMsg: '用户名长度至少为6'
  }
])

Validator.prototype.add = function (dom, rules, errorMsg) {
  for (let i = 0, rule; rule = rules[i++];) {
    const ary = rule.strategy.split(':')
    const errorMsg = rule.errorMsg

    this.cache.push(function () {
      const strategy = ary.shift()
      ary.unshift(dom.value)
      ary.push(errorMsg)
      return strategies[strategy].apply(dom, ary)
    })
  }
}