逐步優雅的表單驗證

緣起

最近被 Jasmine 產品大大的需求耽擱了挺長時間,許久沒落筆,內心有點惶恐,因此特此沉澱以緩解焦慮😂。今天主要分享的是關於表單驗證的一些知識,你們應該都曉得,就是驗證用戶名、郵箱、手機號啥的,雖然食之無味,但棄之惋惜😬。
一般來講表單驗證能夠分爲兩種:即時驗證(本地校驗)和異步驗證(好比用戶名是否可用、驗證碼等),能夠理解爲就是前端校驗和後端校驗(工做中先後端都是要校驗的,以保證最終數據的準確性和有效性,相信你們也應該都有校驗 😁),而咱們今天主要講解的就是前端的表單校驗。前端

目標

👌,首先咱們簡要說下要實現的目標功能:node

  • 具備基礎的表單驗證功能
  • 提供一些內置驗證規則
  • 提供對外開放的能力

事實上表單驗證是能夠脫離頁面存在的,它本質上就是一個函數,接受兩個參數(數據和規則),而後進行校驗,若是校驗出錯則返回相應的錯誤信息。意思你們應該都明白,也都寫過,但如何寫的優雅點呢,或者讓開發使用起來更方便呢,讓咱們從 0 到 1 往下看吧🧐。後端

初版

So,萬事開頭難🤨,該從何下手呢?很顯然,咱們的思路就兩步:數組

  1. 首先獲取到要校驗的值和規則;
  2. 而後進行相應的規則校驗,並返回校驗結果。

具體點說就是咱們要寫一個函數並傳遞兩個參數(數據和規則),另外它還應該返回個錯誤對象,就像下面這樣👇:markdown

function validate(data, rules) {
    // ...
}
// 數據大概長這樣
let data = {
    name: 'xxx',
    phone: '138xxxxxxxx'
}
// 規則大概長這樣,爲何長這樣,你用過一些 UI 框架應該會有點感受
let rules = [{
    key: 'name',
    required: true,
    minLen: 6,
    maxLen: 10
}, {
    key: 'phone',
    pattern: 'phone'
}]
// 錯誤信息大概長這樣
let errors = {
    name: {
        required: '必填',
        minLen: '過短了',
        maxLen: '太長了'
    },
    phone: {
        pattern: '手機格式不對'
    }
}
複製代碼

上面這段看似簡單的代碼其實暗藏玄機,這裏我主要強調如下兩三個點:框架

  • 規則 rules 是一個數組,爲何呢,由於在實際工做中咱們時常須要按順序校驗,因此要寫成數組的形式,咱們應該是根據 rules 的順序去校驗對應的 data。
  • 每一個數據返回的錯誤信息可能有多個,咱們是隻展現一個仍是都展現呢?你們能夠思考一下下🤔。。。ok,謎底揭曉,一般咱們只要記錄一個錯誤便可,由於在頁面上通常只展現一個錯誤提示,也就是說某個數據錯了,就不要驗證該數據的其餘錯誤了,沒有那個必要,不過本篇文章會把錯誤全都展現出來😯,哈哈。
  • 另外,每一個 rule 中的 required 字段的優先級老是最高的,它相對於其餘規則比較特殊,畢竟值都沒有,要其它規則有何用。

而後咱們只要完善驗證函數就好了,大致思路就是循環 rules,拿到對應的 data 值進行校驗,若有錯誤就寫到 errors 裏面,就像下面這樣👇:異步

function validate(data, rules) {
    let errors = {};  // 有錯誤的話放這裏面
    rules.forEach(rule => {
        let val = data[rule.key]
        if (rule.required) {
            if (!val && val !== 0) {
                setDefaultObj(errors, rule.key) // 這個函數在下面,目的是爲了確保 errors[rule.key] 是個對象
                errors[rule.key].required = '必填'
                return // 若是沒填就直接 return 了,不須要再進行此數據的其餘校驗
            }
        }
        if (rule.pattern) {
            if (rule.pattern === 'phone') {
                if(!/^1\d{10}$/.test(val)) { // 簡單校驗了一下手機
                    setDefaultObj(errors, rule.key)
                    errors[rule.key].pattern = '手機格式錯誤'
                }
            }
        }
        if (rule.minLen) {
            if (val.length < rule.minLen) {
                setDefaultObj(errors, rule.key)
                errors[rule.key].minLen = '過短啦'
            }
        }
        if (rule.maxLen) {
            if (val.length > rule.maxLen) {
                setDefaultObj(errors, rule.key)
                errors[rule.key].maxLen = '太長啦'
            }
        }
        console.log(errors)
    });
}
function setDefaultObj(obj, key) { // 確保是個對象,以便於賦值
    obj[key] = obj[key] || {}
}
複製代碼

讓咱們用 node 執行一下上面這個函數,能夠看到以下結果: 沒錯,以上就是咱們初版的全部代碼,已經寫完了😎,內容很少也好理解。
可是這還遠遠不夠,雖然基礎功能實現了,但缺點也是極其明顯的:函數

  • 要是再多來幾個校驗,這函數得胖到什麼程度
  • 過多的 if-else 說明咱們須要讓它優雅點
  • 沒有什麼可複用性
  • 還有些看似重複的邏輯
  • 若是咱們要改個規則還要到函數裏面改,違反了開放-封閉的原則

因此讓咱們來小改一下吧🤨(小改怡情,大改傷身),固然你仍是能夠先思考一下🤔。。。ui

第二版

咱們首先能想到的是把 if-else 拿出來,把校驗的邏輯提取到外面,那咋提呢?咱們都知道函數其實也是個對象,因此能夠把校驗方法直接寫到函數的屬性中,就像 fn.required = () => {} 或者 fn.pattern = () => {} 這個樣子,下面是改完以後的具體代碼👇:this

function validate(data, rules) {
    let errors = {};  // 有錯誤的話放這裏面
    rules.forEach(rule => {
        let val = data[rule.key]
        if (rule.required) {
            let error = validate.required(val)
            if (error) {
                setDefaultObj(errors, rule.key)
                errors[rule.key] = error
                return
            }
        }
        if (rule.pattern) {
            let error = validate.pattern(val, rule.pattern)
            if (error) {
                setDefaultObj(errors, rule.key)
                errors[rule.key].pattern = error
            }
        }
        if (rule.minLen) {
            let error = validate.minLen(val, rule.minLen)
            if (error) {
                setDefaultObj(errors, rule.key)
                errors[rule.key].minLen = error
            }
        }
        if (rule.maxLen) {
            let error = validate.maxLen(val, rule.maxLen)
            if (error) {
                setDefaultObj(errors, rule.key)
                errors[rule.key].maxLen = error
            }
        }
        console.log(errors)
    });
}
validate.required = (val) => {
    if (!val && val !== 0) {
        return '必填'
    }
}
validate.pattern = (val, pattern) => { // pattern 能夠是用戶自定義的正則也能夠是內置的
    if (pattern === 'phone') {
        if(!/^1\d{10}$/.test(val)) {
            return '手機格式錯誤'
        }
    } else if(!pattern.test(val)) {
        return '手機格式錯誤'
    }
}
validate.minLen = (val, minLen) => {
    if (val.length < minLen) {
        return '過短啦'
    }
}
validate.maxLen = (val, maxLen) => {
    if (val.length > maxLen) {
        return '太長啦'
    }
}
複製代碼

改完一看,你可能會臥槽🤐,代碼量好像沒什麼減小,甚至重複的更明顯了,至關不優雅呀。臥槽雖然沒錯,但和第一個版本相比,你能夠看到咱們把規則抽離出來了,至少不會都塞在 validate 函數裏,你能夠專心地在函數外面修改對應的規則,也可在函數外面添加其它規則。
可是這樣還不夠,剛纔說的幾個缺點好像也還在,尤爲是感受下面這一段很重複,你能夠看到每一個 if-else 都寫的差很少,就一個單詞不同,說明咱們能夠繼續改寫它。具體怎麼改寫,又能夠思考一下了🤔。。。

if (rule.required) {}
if (rule.pattern) {}
if (rule.minLen) {}
if (rule.maxLen) {}
複製代碼

第三版

很簡單的一個想法就是遍歷它,只不過咱們要注意的是每一個 rule 裏面的 key: 'xxx'required: true 是比較特殊的,咱們要將他們排除在外,遍歷其它規則便可,其它規則能夠看作是平等的。具體看下面的代碼👇,有註釋應該都能懂🙄:

function validate(data, rules) {
    let errors = {};  // 有錯誤的話放這裏面
    rules.forEach(rule => {
        let val = data[rule.key]
        if (rule.required) { // required 比較特殊,單獨處理比較合適
            let error = validate.required(val)
            if (error) {
                setDefaultObj(errors, rule.key)
                errors[rule.key] = error
                return
            }
        }
        let restKeys = Object.keys(rule).filter(key => key !== 'key' && key !== 'required'); // 過濾掉 key 和 required
        restKeys.forEach(restKey => {
            if (validate[restKey]) { // 這裏要注意規則可能不存在,這時候須要給用戶一個警告或者報錯
                let error = validate[restKey](val, rule[restKey])
                if (error) {
                    setDefaultObj(errors, rule.key)
                    errors[rule.key][restKey] = error
                }
            } else {
                throw `${restKey} 規則不存在`
            }
        })
    });
    console.log(errors)
    return errors
}
複製代碼

哈哈😊,如今看起來好想舒服點了,不過仍是略顯彆扭,通用性和擴展性好像也不夠強。假若有人把這個東東改了,會影響到其餘人嗎?又或者規則一多會衝突嗎?因此,問題仍是有的。
事實上咱們如今的校驗是公用的,而咱們須要把規則分爲兩種,一種是公用的,一種是自定義的(可覆蓋公用而且不影響他人)。什麼意思呢,其實就是用原型鏈和繼承來改寫啦😁,又由於如今有了 ES6,咱們就不用 prototype 來寫了,而是用 class,實際上他們是同樣的,語法糖嘛,好吃方便😋。

第四版

好的,如今讓咱們用 class 來重寫上面的校驗函數(不懂 class 寫法的建議先去看一下,挺簡單的,就是換個寫法,習慣就好),這裏直接上代碼👇:

class Validator {
    constructor() {
    }
    static addRule (name, fn) { // 全局添加新規則
        Validator.prototype[name] = fn
    }
    validate(data, rules) {
        let errors = {}
        rules.forEach(rule => {
            let val = data[rule.key]
            if (rule.required) {
                let error = this.required(val)
                if (error) {
                    this.setDefaultObj(errors, rule.key)
                    errors[rule.key] = error
                    return
                }
            }
            let restKeys = Object.keys(rule).filter(key => key !== 'key' && key !== 'required');
            restKeys.forEach(restKey => {
                if (this[restKey]) {
                    let error = this[restKey](val, rule[restKey])
                    if (error) {
                        this.setDefaultObj(errors, rule.key)
                        errors[rule.key][restKey] = error
                    }
                } else {
                    throw `${restKey} 規則不存在`
                }
            })
        });
        console.log(errors)
    }
    required (val) {
        if (!val && val !== 0) {
            return '必填'
        }
    }
    pattern (val, pattern) { // pattern 能夠是用戶自定義的正則也能夠是內置的
        if (pattern === 'phone') {
            if(!/^1\d{10}$/.test(val)) {
                return '手機格式錯誤'
            }
        } else if(!pattern.test(val)) {
            return '手機格式錯誤'
        }
    }
    minLen (val, minLen) {
        if (val.length < minLen) {
            return '過短啦'
        }
    }
    maxLen (val, maxLen) {
        if (val.length > maxLen) {
            return '太長啦'
        }
    }
    setDefaultObj (obj, key) {
        obj[key] = obj[key] || {}
    }
}
// 固然,使用方法也得跟着變,可是打印的錯誤信息是同樣的
let validator = new Validator()
validator.validate(data, rules)
複製代碼

是否是有點撥開雲霧見月明的感受🤯?沒有就算了😂,反正上面的這個寫法和最初的第一個版本相比應該是跨出一小步了,也易於擴充和維護,挺好👏👏👏。固然你也能夠在第四版的基礎上批閱十載、增刪五次,讓它變得更加完善和優雅。

尾流

回到實際工做中,咱們每每是寫了 if-else 以後就不想去改它了,這應該是比較尷尬的一點了😂,我也是。 不過言而總之,想要寫的優雅,就要多寫多改,比原來好就是進步,這是個按部就班的過程,而不是一步到位。最後但願本文可以對你們有所幫助,大讚無疆啦👍👍👍。。。 ps : 寫完文章後產品大大忽然給我小講了一下下個需求,我一聽,又要好久才能提筆了。

相關文章
相關標籤/搜索