逐步优雅的表单验证

缘起

最近被 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 : 写完文章后产品大大忽然给我小讲了一下下个需求,我一听,又要好久才能提笔了。

相关文章
相关标签/搜索