本身寫表單校驗插件

1、表單驗證模塊的構成

任何表單驗證模塊都是由 配置——校驗——報錯——取值 這幾部分構成的。vue

  1. 配置: 配置規則 和配置報錯,以及優先級node

  2. 校驗: 有在 change 事件校驗, 在點擊提交按鈕的時候校驗, 固然也有在input事件取值的ajax

  3. 報錯: 報錯方式通常要分,報錯的文字有模板,也有自定義的後端

  4. 取值: 將經過驗證的數據返還給開發者調用數組

下面是我老大針對公司項目給我提出的要求app

  1. 集中式的管理 校驗規則 和 報錯模板。dom

  2. 報錯時機可選函數

  3. 校驗正確後的數據,已經打包成對象,能夠直接用ui

  4. 容許各個頁面對規則進行覆蓋,對報錯信息進行自定義修改,以及容許ajax獲取數據後,再對規則進行補充this

  5. 按順序來校驗,在第一個報錯的框彈出錯誤

我就很好奇地問, 爲何要這樣子呢?而後老大就跟我一條一條解答:

  1. 集中式管理規則,和報錯模板的好處,就是規則能夠全局通用,一改全改。老大跟我說,光是暱稱的正則就改了三次。若是這些正則寫在各個頁面,o( ̄ヘ ̄o#)哼,你就要改N個頁面了

  2. pc和移動的流程不同,pc不少校驗都要在change事件或者input事件就校驗並報錯了,而移動則通常是要到提交按鈕再進行校驗。因此寫插件的時候要作好兩手準備。而後,報錯用的ui要能夠支持咱們如今用的layer插件。固然之後這個報錯的ui也可能變,因此你懂滴。

  3. 固然原來jq時代,咱們的公用表單驗證,就能驗證完了,把數據都集合到一個對象裏。這樣ajax的時候,就不用再去取值了。你這個插件耶要達到這個效果

  4. 原來jq的那個公用腳本,正則和報錯都集中到一個地方去了,在不少地方已經很方便了。可是在一些頁面須要改東西的時候還不夠靈活。像RealName這個規則,最先是針對某個頁面配置的,用的是後端接口上的字段名。另外一個支付頁,後端接口上的字段名改爲了PayUser了,可是正則仍是RealName的,原來咱們是要複寫一下RealName。這個就不太方便也很差看了。另一個,支付金額,有最大值和最小值的限制,這個須要從後端獲取的。你也要考慮這個狀況。要作到各個頁面上也能有一些靈活的地方能夠修改規則,自定義報錯等等。

  5. 爲何要按順序校驗啊?你忘了上次牛哥讓咱們輸入框,從上到下,按順序報錯。否則用戶都不知道哪一個地方錯了。還有規則也是要按順序的。哦哦哦。看來此次我放東西的時候,要用下數組了。儘可能保持順序。

我聽了以後,大體懂了,原來以前本身寫的jq表單驗證還有這麼多不舒服的點。-_-|||
接下來,是看看vue給個人好東西。讓我來寫

2、Vue 的插件怎麼寫

我一個vue小白,怎麼就開始寫vue插件了呢?那是由於想解決方案的時候,翻Vue文檔翻到了這裏

這些東東,等我寫完va.js的時候,感受尤大寫的真的是很清楚了。

其實我是想寫個指令來完成表單驗證的事的。結果發現可能有2-3個指令,並且要再Vue.prototype上定義些方法,好讓各個子實例內部也能拓展規則。因而老大說,這就至關於插件了。這讓我非常吃鯨。

va.js主要用的是 Vue指令

Vue 文檔真的寫得很用心,可是我再補充一點吧
vnode.context 就是Vue的實例
咱們作項目的時候,常常一個根組件上掛着N個子組件,子組件上又可能掛着N個子組件。vnode.context獲取的實例,是綁定該指令的組件的實例。這個就至關好用了。你能夠作不少事情

固然還用了點Vue.prototype

Vue.prototype.$method 就是能夠在各個組件上調用的方法。能夠在組件內部用 this.$method調用的

## 3、具體實現的思路 ##

核心思路以下圖:

  • 規則的構造函數

//va配置的構造函數 function VaConfig(type, typeVal, errMsg, name, tag){ this.type = type, this.typeVal = typeVal, this.errMsg = errMsg, this.name = name, this.tag = tag }
  1. type: nonvoid(非空), reg(正則), limit(區間), equal(與某個input相等),unique(不能相同)

  2. typeVal: 根據不一樣type設置不一樣的值

  3. errMsg: 自定義的報錯信息

  4. name: 用來傳ajax的字段,如Password, Username

  5. tag:用來報錯的名字,如‘銀行帳號’,‘姓名’

設置了三種規則

1.默認規則: 只要綁定指令,就默認有的校驗。 好比非空的校驗。 能夠額外加修飾符來去除
2.選項規則: 經過Vue指令的修飾符添加的規則。
3.自定義規則: Vue指令屬性值上添加的規則。
同一個type的規則只存在一個,也就是說,若是type爲reg(正則),那麼會互相覆蓋。 
覆蓋的優先級: 自定義規則 > 選項規則 > 默認規則

思路講的多了。也不知道怎麼講了,下面你們直接看源碼把。

源碼

var Vue
var checkWhenChange = true  //每一個輸入框須要離焦即校驗

// 給一個dom添加class
function addClass(dom, className){
  // if (dom.classList){
  //   dom.classList.add(className);
  // }else{
  //   dom.className += ' ' + className;
  // }

  var hasClass = !!dom.className.match(new RegExp('(\\s|^)' + _class + '(\\s|$)'))
  if(!hasClass){
    dom.className += ' ' + _class
  }
}

//經常使用正則表
var regList = {
  ImgCode: /^[0-9a-zA-Z]{4}$/,
  SmsCode: /^\d{4}$/,
  MailCode: /^\d{4}$/,
  UserName: /^[\w|\d]{4,16}$/,
  Password: /^[\w!@#$%^&*.]{6,16}$/,
  Mobile: /^1[3|4|5|7|8]\d{9}$/,
  RealName: /^[\u4e00-\u9fa5|·]{2,16}$|^[a-zA-Z|\s]{2,20}$/,
  BankNum: /^\d{10,19}$/,
  Money: /^([1-9]\d*|[0-9]\d*\.\d{1,2}|0)$/,
  Answer: /^\S+$/,
  Mail: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/
}

// 斷言函數
function assert(condition, message){
  if(!condition){
    console.error('[va-warn]:' + message)
  }
}

// Rule構造器
function Rule(ruleType, ruleValue, errMsg){
  this.ruleType = ruleType
  this.ruleValue = ruleValue
  this.errMsg = errMsg || ''
}

//VaForm構造器
function VaForm(el, finalRules, modifiers){
  this.ruleOrder = []
  this.rules = {}
  this.dom = el
  this.value = el.value   //值的副本
  this.validated = false  //是否被驗證過
  this.tag = el.getAttribute('tag')   //提示的字段名
  // this.correctMsg = `${this.tag}輸入正確!`
  this.correctMsg = ''
  this.modifiers = modifiers   //一些特殊的配置
  this.noCheck = false         //爲true則不要校驗

  this.ruleOrder = finalRules.map(item=>{
    this.rules[item.ruleType] = item
    return item.ruleType
  })
}

//rules中靠前的配置優先級最高
function mergeRule(...rules){
  var mergeResult = []
  var combineArr = Array.prototype.concat.apply([], rules)
  var hash = {}
  combineArr.forEach((rule)=>{
    if(hash[rule.ruleType] === undefined){
      mergeResult.push(rule)
      hash[rule.ruleType] = mergeResult.length - 1
    }else{
      var index = hash[rule.ruleType]
      Object.assign(mergeResult[index], rule)
    }
  })
  return mergeResult
}

//單個規則的驗證結果
function VaResult(ruleType, ruleValue, isPass, errMsg){
  this.ruleType = ruleType
  this.ruleValue = ruleValue
  this.isPass = isPass
  this.errMsg = errMsg
}

// 顯示結果的構造器
function DisplayResult(isPass, message){
  this.isPass = isPass
  this.message = message
}

//單個規則的校驗,或者單個表單的校驗
function validate(field, ruleType){
  assert(field, '未輸入要驗證的字段')
  var vaForm = this.forms[field]
  var {ruleOrder, rules} = vaForm

  if(ruleType === undefined){
    return this.checkForm(vaForm)
  }else{
    var rule = rules[ruleType] //規則
    return this.checkRule(vaForm, rule)
  }
  // vaForm.validated = true
}

// 得到不一樣的報錯信息
function getErrMsg(vaForm, ruleType, ruleValue){
  var tag = vaForm.tag
  var errMsgs = {
    NonEmpty: `${tag}不能爲空`,
    reg: `${tag}格式錯誤`,
    limit: `${tag}必須在${ruleValue[0]}與${ruleValue[1]}之間`,
    equal:`兩次${tag}不相同`,
    length: `${tag}長度必須在${ruleValue[0]}與${ruleValue[1]}之間`,
    unique: `${tag}不能相同`
  }
  return errMsgs[ruleType]
}

//檢測非空
function checkEmpty(ruleValue, vaForm, va){
  return vaForm.value.trim() ? true : false
}
//檢測正則
function checkReg(ruleValue, vaForm, va){
  return ruleValue.test(vaForm.value) ? true : false
}
//檢測數字區間
function checkLimit(ruleValue, vaForm, va){
  var value = vaForm.value
  return ((+value >= ruleValue[0]) && (+value <= ruleValue[1])) ? true : false
}
//檢測相等
function checkEqual(ruleValue, vaForm, va){
  var target = va.forms[ruleValue]
  return target.value === vaForm.value ? true : false
}
//檢測字符長度
function checkCharLength(ruleValue, vaForm, va){
  var length = vaForm.value.length
  return ((+length >= ruleValue[0]) && (+length <= ruleValue[1])) ? true : false
}

//幾個輸入框要各不相同
function checkUnique(ruleValue, vaForm, va){
  var uniqueGroup = va.uniqueGroup[ruleValue]
  var values = uniqueGroup.map(field=>va.forms[field].value)
  var uniqueValues = values.filter((item,index,arr)=>arr.indexOf(item) === index)
  return values.length === uniqueValues.length ? true : false
}

// 檢測單個規則
function checkRule(vaForm, rule){
  var forms = this.forms
  var {ruleType, ruleValue, errMsg} = rule
  //若是有自定義報錯就按自定義報錯,沒有就格式化報錯
  errMsg = errMsg || getErrMsg(vaForm, ruleType, ruleValue)

  var ruleCheckers = {
    NonEmpty: checkEmpty,
    reg: checkReg,
    limit: checkLimit,
    equal: checkEqual,
    length: checkCharLength,
    unique: checkUnique
  }

  var ruleChecker = ruleCheckers[ruleType]
  var isPass = ruleChecker(ruleValue, vaForm, this)
  var vaResult = new VaResult(ruleType, ruleValue, isPass, isPass ? null : errMsg)
  return vaResult
}

//檢測單個表單
function checkForm(vaForm){
  var results = vaForm.ruleOrder.map(ruleType=>{
    var rule = vaForm.rules[ruleType]
    return this.checkRule(vaForm,rule)
  })

  var errIndex = null
  for(var i = 0;i < results.length;i++){
    var result = results[i]
    if(result.isPass === false){
      errIndex = i
      break
    }
  }

  if(errIndex === null){
    return new DisplayResult(true,  vaForm.correctMsg)
  }else{
    return new DisplayResult(false, results[errIndex].errMsg)
  }
}

//刷新vaForm中的值的數據
function refreshValue(field, newValue){
  this.forms[field].value = newValue + ''
}

//更新全部表單的值
function refreshAllValue(){
  this.fieldOrder.forEach(field=>{
    var vaForm = this.forms[field]
    vaForm.value = vaForm.dom.value
  })
}

// 校驗全部的表單,並彈出第一個錯誤。考慮能夠爲空的狀況
function checkAll(){
  var firstErr = null
  this.fieldOrder.forEach(field=>{
    var vaForm = this.forms[field]
    var canNull = vaForm.ruleOrder.every(ruleType=>ruleType !== 'NonEmpty')  //輸入框能夠爲空
    var noCheckEmpty = (vaForm.value === '' && canNull)   //該輸入框能夠爲空,且輸入爲空

    if(vaForm.noCheck === false && noCheckEmpty === false){
      var result = this.setVmResult(field)
      // var result = this.validate(field)
      // this.vmResult[field] = result
      // vaForm.validated = true

      if(firstErr === null && result.isPass === false){
        firstErr = result.message
      }
    }

  })
  return firstErr
}

//驗證單個字段,返回值,並彈出報錯
function setVmResult(field){
  var result = this.validate(field) //本輸入框結果
  this.vmResult[field] = result    //將報錯彈出
  this.forms[field].validated = true  //校驗過了
  return result
}

// 返回各個表單的值對象
function getValue(){
  var dataSet = {}
  for(var field in this.forms){
    dataSet[field] = this.forms[field].value
  }
  return dataSet
}

//添加一個規則
function addRule(field, index, Rule){
  var vaForm = this.forms[field]
  vaForm.ruleOrder.splice(index, 0, Rule.ruleType)
  vaForm.rules[Rule.ruleType] = Rule
}

// function resetAll(){
//   this.fieldOrder.forEach(field=>{
//     this.refreshValue(field, '')
//   })
// }

// 設置不校驗的表單
function setNoCheck(field, bool){
  this.forms[field].noCheck = bool
}

function createVa(vm, field){
  var va = {
    vmResult:vm.va,
    fieldOrder:[],
    forms:{},
    group:{
      base:[],
    },
    equalGroup:{},                  //必須相等的字段
    uniqueGroup:{},                 //必須不一樣的字段
    Rule:Rule,                      //Rule構造器
    VaForm:VaForm,                  //VaForm構造器
    validate: validate,             //暴露的校驗函數
    setVmResult: setVmResult,       //校驗並報錯
    checkRule: checkRule,           //內部的校驗單條規則的函數
    checkForm: checkForm,           //內部的校驗單個表單的函數
    refreshValue: refreshValue,     //更新某個表單的值
    checkAll: checkAll,             //檢查全部的函數
    getValue: getValue,             //獲取全部表單的當前值,獲得一個對象
    setNoCheck:setNoCheck,          //設置爲不校驗
    addRule:addRule,                //給一個表單添加一個規則
    refreshAllValue:refreshAllValue //更新全部表單的值
    // resetAll: resetAll
  }

  if(vm.$va){
    return vm.$va
  }else{
    vm.$va = va
    return va
  }
}

//v-va:Password.canNull = "[{reg:/^\d{4}$/}]"
//arg = Password,  modifiers.canNull = true, value爲後面相關的
//arg用來存字段名, modifiers用來存特殊配置, value爲規則, tag是中文提示名, group 爲分組
var main = {}
main.install = function(_Vue, options){
  Vue = _Vue

    Vue.directive('va',{
    bind:function(el, binding, vnode){
      var vm = vnode.context                         //當前的vue實例
      var field = binding.arg === 'EXTEND' ? el.getAttribute('name') : binding.arg // 當arg爲EXTEND,從name屬性得到值
      var option = binding.modifiers                    //特殊配置(容許非空,編輯新增共用等)
      var value = el.value                              //輸入框的初始值
      var group = el.getAttribute('group') || 'base'    //分組,一個表單框在多個組呢?這個還沒設,要兼容。 經過相似 'group1 group2 group3 group4'
      var tag = el.getAttribute('tag')
      var regMsg = el.getAttribute('regMsg') || ''   //針對正則的自定義報錯
      var baseRule = []                              //默認的校驗規則             --不用寫,默認存在的規則(如非空),優先級最高
      var customRule = []                            //用戶自定義的規則(組件中) --bingding.value
      var optionalRule = []                          //配置項中引伸出來的規則,優先級最低

      assert(tag, '未設置輸入框的tag')
      assert(vm.va, '實例的data選項上,未設置va對象')  //實例上若是沒有設置結果則報錯。
      assert(field, '未設置輸入框字段')
      var va = createVa(vm, field)  //單例模式建立va,綁定在vm上
      va.fieldOrder.push(field)     //字段的檢驗順序
      va.group[group].push(field)   //分組
      var NonEmpty = new Rule('NonEmpty', true, '')
      //默認非空
      if(option.CanNull === undefined){
        baseRule.push(NonEmpty)
      }

      //若是regList裏有name對應的,直接就加進optionalConfig
      if(regList[field]){
        optionalRule.push(new Rule('reg', regList[field], regMsg))
      }

      //若是modefiers中的字段有在正則表裏,將其加入optionalRule
      var regOptions = Object.keys(option);
      for(var i = 0;i < regOptions.length;i++){
        var regOption = regOptions[i]
        if(regList[regOptions[i]]){
          optionalRule.push(new Rule('reg', regList[regOption], regMsg))
        }
      }

      //用戶自定義的規則
      if(binding.value !== undefined){
        customRule = binding.value.map(item=>{
          var ruleType = Object.keys(item)[0];
          var errMsg = ruleType === 'reg' ? regMsg : ''
          return new Rule(ruleType, item[ruleType], errMsg)
        })
      }

      var finalRules = mergeRule(baseRule, optionalRule, customRule)
      var hasUniqueRule = false
      //對聯合校驗的進行預處理
      finalRules.forEach(rule=>{
        var {ruleType, ruleValue} = rule
        if(ruleType === 'equal'){
          if(va.equalGroup[ruleValue] === undefined){
            va.equalGroup[ruleValue] = [field]
          }else{
            va.equalGroup[ruleValue].push(field)
          }
        }

        if(ruleType === 'unique'){
          hasUniqueRule = ruleValue
          if(va.uniqueGroup[ruleValue] === undefined){
            va.uniqueGroup[ruleValue] = [field]
          }else{
            va.uniqueGroup[ruleValue].push(field)
          }
        }
      })

      var vaForm = new VaForm(el, finalRules, option)
      va.forms[field] = vaForm

      if(checkWhenChange){
        function validateSingle(){
          va.refreshValue(field, el.value)  //更新值
          //若是容許爲空的此時爲空,不校驗
          if(vaForm.value === '' && option.CanNull){
            va.vmResult[field] = {}   //若是爲空,把界面顯示上面的提示清掉
            return
          }

          if(vaForm.noCheck === false){
            va.setVmResult(field)
          }

          var isEqualTarget = false
          for(var index in va.equalGroup){
            if(index === field){
              isEqualTarget = true
            }
          }

          //相等框的聯合校驗
          if(isEqualTarget){
            va.equalGroup[field].forEach(item=>{va.setVmResult(item)})
          }

          //不一樣框的聯合校驗
          if(hasUniqueRule){
            va.uniqueGroup[hasUniqueRule].forEach(item=>{va.setVmResult(item)})
          }
        }

        //在change和blur上都綁定了處理事件
        el.addEventListener('change', validateSingle)
        el.addEventListener('blur', validateSingle)
      }

    },
  })
}

export default main

 

如今項目已經用起來了。固然表單驗證這種是高度定製化的。純粹分享個過程和思路。也算我這個vue新手的一次階段性成果吧。哈哈~

使用實例

第一個框,加了兩條指令

  1. v-va:Password 這個表明使用配置表中password對應的配置(包括非空和正則,默認規則),同時應用Password做爲校驗成功獲取的 數據對象的key

  2. tag爲報錯顯示中此輸入框的名字

第二個框,爲確認框,也加了兩個指令
1.v-va:checkPassword.Password = "[{'equal':'Password'}]"
通常v-va後面的第一個字段爲數據對象的key,他和正則對應的名字有可能不一樣。
這個字段若是和配置表中的配置匹配,那麼天然應用配置。
若是不匹配,就要本身在後面用.的方式加配置(選項規則)。像這裏的Password。

最後面還有一個 屬性值 "[{'equal':'Password'}]"(自定義規則)。
這個地方用了數組,即會按這個數組的配置來進行校驗。
同時這個數組有順序,順序表明規則的優先級。 
這個配置表明,這個框必須和上面那個Password的框值相等,不然報錯。
另外確認框不加入最後的結果數據對象。

2.tag 用來做爲報錯信息的名字

校驗觸發按鈕 上面有一個指令 v-va-check
1.用來觸發校驗
2.校驗成功後,將數據對象存在實例的vaVal屬性下

根據上面的實例

規則的優先級:
1.自定義規則 > 選項規則 > 默認規則 
2.規則中的優先級依照數組順序

另外,能夠看到爲了使用者方便,我在咱們團隊中事先作了一些約定,並可能會用到 v-va、v-va-check、tag等指令,佔用了實例的兩個屬性名vaConfig、vaVal。這些約定和設置可使使用者使用方便(經過配置控制校驗時機, 校驗成功後天然生成經過的數據對象,自定義報錯信息等等)。可是也減小了這個插件的普適性。

此方案僅提供各位作思路參考。我的認爲,表單驗證是高度定製化的需求,儘可能根據各個業務狀況進行取捨。在個人方案中,並不像vue-validator同樣作了髒校驗

相關文章
相關標籤/搜索