缘起
最近被 Jasmine 产品大大的需求耽搁了挺长时间,许久没落笔,心里有点惶恐,所以特此沉淀以缓解焦虑😂。今天主要分享的是关于表单验证的一些知识,大家应该都晓得,就是验证用户名、邮箱、手机号啥的,虽然食之无味,但弃之可惜😬。
通常来说表单验证可以分为两种:即时验证(本地校验)和异步验证(比如用户名是否可用、验证码等),可以理解为就是前端校验和后端校验(工作中前后端都是要校验的,以保证最终数据的准确性和有效性,相信大家也应该都有校验 😁),而我们今天主要讲解的就是前端的表单校验。
目标
👌,首先我们简要说下要实现的目标功能:
- 具有基础的表单验证功能
- 提供一些内置验证规则
- 提供对外开放的能力
事实上表单验证是可以脱离页面存在的,它本质上就是一个函数,接受两个参数(数据和规则),然后进行校验,如果校验出错则返回相应的错误信息。意思大家应该都明白,也都写过,但如何写的优雅点呢,或者让开发使用起来更方便呢,让我们从 0 到 1 往下看吧🧐。
第一版
So,万事开头难🤨,该从何下手呢?很显然,我们的思路就两步:
- 首先获取到要校验的值和规则;
- 然后进行相应的规则校验,并返回校验结果。
具体点说就是我们要写一个函数并传递两个参数(数据和规则),另外它还应该返回个错误对象,就像下面这样👇:
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 说明我们需要让它优雅点
- 没有什么可复用性
- 还有些看似重复的逻辑
- 如果我们要改个规则还要到函数里面改,违反了开放-封闭的原则
所以让我们来小改一下吧🤨(小改怡情,大改伤身),当然你还是可以先思考一下🤔。。。
第二版
我们首先能想到的是把 if-else 拿出来,把校验的逻辑提取到外面,那咋提呢?我们都知道函数其实也是个对象,所以可以把校验方法直接写到函数的属性中,就像
fn.required = () => {}
或者
fn.pattern = () => {}
这个样子,下面是改完之后的具体代码👇:
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