求关注,求收藏,求点赞,如果发现博主有写的不合理的地方请及时告知,谢谢~
~
最近在参与年终的项目复盘,在代码部分看了几个项目的前端代码,发现UI框架都是使用的IView或者ElementUI,其中在表单部分的输入框这一块发现了一些关于校验方式可以进一步优化使用的问题…
前言
表单控件,可以说是在前端所有控件中的绝对的基石之一,几乎任何网站都逃脱不了增、删、改、查这个四个基础接口,而为了实现这四个接口对应的功能,在前端几乎必不可少的就是需要输入框来做一些数据的输入和选择 ,比如新增用户时的用户名、密码、地址等等,而为了验证输入信息的有效性,自然就有了对输入信息的校验;
有的小伙伴可能会说,信息的校验不应该是后台在做吗?确实是,不可否认的是后台确实需要做信息的校验,但是如果验证仅仅是在后端做,那么是不是在体验上会有很大的问题,比如:用户名,用户名仅允许数字加字母,如果仅仅是这种验证就需要发送到后台去验证一下,那也太不科学了,而且万一网络不好,或者一些其他原因接口返回的比较慢,那用户体验就会非常差,因此,不可避免的前台也需要做校验, 你想,如果用户输入的内容不合法,在下一秒就可以直接得到错误反馈,那么是不是在体验上就得到了大幅的提升…
先看一下基础用法,也是现在非常绝大多数基于UI框架采用的方式
基础用法
我们项目中的UI框架基本都是使用的IView,其实包括ElemenUI在内的这两者的表单验证都是使用的async-validator,先看一下IView官网的例子,
<template>
<Form ref="formValidate" :model="formValidate" :rules="ruleValidate" :label-width="80">
<FormItem label="Name" prop="name">
<Input v-model="formValidate.name" placeholder="Enter your name"></Input>
</FormItem>
<FormItem label="E-mail" prop="mail">
<Input v-model="formValidate.mail" placeholder="Enter your e-mail"></Input>
</FormItem>
</Form>
</template>
<script>
export default {
data () {
return {
formValidate: {
name: '',
mail: '',
},
ruleValidate: {
name: [
{
required: true, message: 'The name cannot be empty', trigger: 'blur' }
],
mail: [
{
required: true, message: 'Mailbox cannot be empty', trigger: 'blur' },
{
type: 'email', message: 'Incorrect email format', trigger: 'blur' }
]
}
}
}
}
</script>
这是官网的例子,裁剪了一部分代码,原版代码太多了,在这次查看的项目中同样发现了大量这种较为基础的用法,这种用法对对于需要在一个界面上放十几个输入框的设计,会存在大量重复rules代码,并且重复的量还特别太夸张,比如,我增见到过一个弹窗,光是输入框就将近15个,写了光是这个ruleValidate就写了100多行…直呼牛皮…
可能有很多小伙伴发现,这种写法中除了message提示的信息不一样,其它配置都是完全一致的,从共性抽离的角度来讲,那不是完全可以抽离?
优化
方式一:抽离公共函数
既然是优化,那么首先需要确认的就是可优化点,正如上面所说,经过分析发现,大量输入框的校验只有一种,必输项,换个说法就是这个输入框必须输入,除此之外没有别的限制,但是仅仅这一个需求,最后可能演变的代码就变成了这样:
ruleValidate: {
name: [
{
required: true, message: 'The name cannot be empty', trigger: 'blur' }
],
mail: [
{
required: true, message: 'Mailbox cannot be empty', trigger: 'blur' }
],
city: [
{
required: true, message: 'Please select the city', trigger: 'blur' }
],
gender: [
{
required: true, message: 'Please select gender', trigger: 'blur' }
],
desc: [
{
required: true, message: 'Please enter a personal introduction', trigger: 'blur' }
]
}
其实完全没有必要,不同数组之间最大的区别也就是提示的文字不同,我们完全可以抽离一个公共函数来做统一处理,比如,我们期望改完后关于验证是这么写的:
ruleValidate: {
name: Validate("The name cannot be empty"),
mail: Validate("Mailbox cannot be empty"),
city: Validate("Please select the city"),
gender: Validate("Please select gender"),
desc: Validate("Please enter a personal introduction")
}
这不比每一次校验输入一个数组,每一次都要写一遍来的轻松,而且实现也并不复杂,最简单的如下:
function Validate(msg)(
return [
{
required: true, message: msg, trigger: 'blur' }
]
)
有的小伙伴可能会说,这不行啊这使用场景太局限了,就只能验证必输项,太鸡肋…到这里仅仅是抛砖引玉,只是提供了个思路,在实用性上还是有很大缺陷的,如果有小伙伴希望能具体细聊一下,那我们就简单说下,一般常规的验证规则大致可以分为以下几种:
- 仅必输项,无其他验证规则;
- 仅允许英文字母;
- 仅允许数字;
- 仅允许英文字母和数字;
那我们就预设几个参数,分别对应:提示信息,是否必输,验证方式,大致运行流程如下:
流程图
效果
经过转换后,效果依旧是原来的效果,并没有影响其正常使用,错误提示依然是借助于IView或者ElementUI的错误提示实现
期望结果
期望输入
handleParams({
messgage:"用户名不能为空"},"code")
期望输出
[
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
{
// validate:根据参数实现的自定义校验
validator: validate(isCode, "请输入数字或字母"),
trigger: "blur",
}
]
实现
- 第一步就是对参数的初始化,在这一步中,我们需要对参数进行简略的处理,并且定义一个默认参数,大致如下代码;
// 将参数转成数组,方便处理
const args = [].slice.call(arguments);
// 默认配置
const DEFAULT_MESSAGE = {
required: true,
message: "该输入项为必填项",
trigger: "blur",
};
- 处理参数,根据具体的参数的转成对应的合法参数并返回,这里需要对输入做一个合法性进行判断;
// 处理参数
const params = handleParams(args);
/**
* 处理参数
* @param {Array} array 参数
* @returns 合法值
*/
const handleParams = (array) => {
const result = {
type: "",
require: null,
};
if (array.length === 0) {
result.require = DEFAULT_MESSAGE;
} else {
for (let item = 0; item < 2; item++) {
if (utils.isString(array[item])) {
result.type = array[item];
} else if (utils.isObj(array[item])) {
result.require = array[item];
// 判断用户是否手写了require,默认true
result.require.required = utils.hasProperty(
array[item],
"required"
)
? array[item].required
: true;
}
}
if (result.require === null) {
result.require = DEFAULT_MESSAGE;
}
}
return result;
};
- 生成对应类型的自定义校验规则;
if (params.type !== "") {
switch (params.type) {
case "code":
rules.push({
validator: validate(isCode, "请输入数字或字母"),
trigger: "blur,change",
});
break;
case "mobile":
rules.push({
validator: validate(isMobile, "请输入正确的手机号"),
trigger: "blur,change",
});
break;
case "number":
rules.push({
validator: validate(isNumber, "请输入数字"),
trigger: "blur,change",
});
break;
case "letter":
rules.push({
validator: validate(isLetter, "请输入字母"),
trigger: "blur,change",
});
break;
default:
rules.push({
});
break;
}
}
小结
简单的来说就是实现差不多就是这样,可能有不完善的地方,但大致方向没有问题,之后便是扩展了,扩展的话需要根据自身的需求进行进一步可扩展了;
回过头来想我们抽离的最终目的,最终我们的目的就是为了实现:
- 减少书写的代码量,过于频繁的书写校验规则和必输这些数组实在浪费时间;
- 统一管理校验代码,尤其是协同开发的时候,一个人在某个业务场景里写了一遍自定义校验了,如果另外一个在不知道的情况下也同样写了一遍,那是不是浪费时间;
- 减少BUG提升开发效率,不可否认的是在一个项目组中,尤其是规模不大的小项目组,并不是每个人都能书写公共方法,很多新人往往考虑不周导致了隐性BUG的存在,如果一开始就有人将工具函数写完整,那么开发效率和BUG可以避免很多
方式二:自定义指令
这种方式其实有点 另类 ,也是我们某个项目中用到的,不是特别正统,但是发展方向我觉得还是没有问题的,它最终实现的效果是, 让输入框只能输入我们期望的合法值,如果输入值不合法,直接会被删除无法输入;这也就导致了它并不能 有效的利用IView或者ElementUI的错误提示,这也就是我为什么说它有点另类的原因;
虽然它没有有效的利用formItem的错误提示,但在体验上影响却不大,必输这一块它依然使用的是async-validator,再加之用法简单,确实有些独到之处;
效果
大致效果如下,是不是相较于正常的错误提示,这种稍微有那么一点点另类…
用法
// 以IView的Input组件为例
<Input v-model="formData.name" v-rules:number>
大致用法就这么简单,当输入的内容不为数字时,直接将该字符串给delete掉,这样就达到了控制输入的目的;
流程图
实现
其实没有太多的技术难点,总结一下,主要的技术点在于以下几个:
- 获取DOM,因为绑定的DOM必须是input或者包含input,而不管是IView还是ElementUI的Input组件都不是单纯的Input,是div以及Input的组合,毕竟涉及到了样式问题,因此第一步需要对绑定DOM的获取与判定;
- 输入时中文输入的异常处理,当正在输入中文的时候,输入框内会存在重复触发的问题,但这个可以通过compositionstart和compositionend解决;
解决了这两个问题,大致代码如下:
获取DOM
getElement(el, type) {
// 容错判断,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("类型错误,绑定对象必须是HTMLElement");
return false;
}
const list = type === "input" ? INPUT : DISABLED_LIST;
const domTagName = el.tagName.toLowerCase();
// 是否在可操作列表中
if (list.indexOf(domTagName) > -1) {
return el.getAttribute("type") === null ||
el.getAttribute("type") === "text"
? el
: false;
}
// 子集是否存在可操作列表中的元素
else {
const domList = [].slice.call(el.querySelectorAll(list.join(",")));
if (domList.length === 0) return false;
// 遍历子集
for (let item = 0; item < domList.length; item++) {
if (
domList[item].getAttribute("type") === null ||
domList[item].getAttribute("type") === "text"
) {
return domList[item];
}
}
return false;
}
},
绑定事件
/**
* 为DOM添加事件
* @param {HTMLElement} el 绑定事件的DOM
* @param {Object} binding 集合
*/
function handleFilter(el, binding) {
switch (binding.arg) {
case "number":
numberFilter(el);
break;
case "letter":
letterFilter(el);
break;
break;
default:
codeFilter(el);
break;
}
}
/**
* 数字验证
* @param {HTMLElement} el 待绑定元素
*/
const numberFilter = function(el) {
exAddListener(el, () => {
el.value = handleNumber(el.value);
});
};
const addListener = function(el, type, fn) {
el.removeEventListener(type, fn, false);
el.addEventListener(type, fn, false);
};
const exAddListener = function(el, fn) {
addListener(el, "compositionstart", (e) => {
e.target.isNeedPrevent = true;
});
addListener(el, "compositionend", (e) => {
e.target.isNeedPrevent = false;
});
addListener(el, "keydown", (e) => {
e.target.keyEvent = true;
});
addListener(el, "keyup", (e) => {
if (e.target.isNeedPrevent) return;
fn();
e.target.keyEvent = false;
});
};
/**
* 字母验证
* @param {HTMLElement} el 待绑定元素
*/
const letterFilter = function(el) {
exAddListener(el, () => {
el.value = el.value.replace(/[^A-Za-z]+/g, "");
});
};
不知有没有小伙伴发现,这里绑定的事件是keyup而不是Input,可能有小伙伴认为绑定input更为合理,这个也是我一开始的疑问,后来经过确认,是因为这样:input会直接在输入的时候有一个校验,它最终实现的效果会导致input里直接无法输入不合法的内容,如果这样,那么用户可能会有“是不是我键盘坏了?”之类的疑问,但是如果是keyup事件,那么此时会有一个时间差,输入时确实输入了内容,只是松开键盘时又将输入的内容删除了,这样用户会有一个交互上的反馈,这将隐性的提示用户输入不合法;想想还挺有道理…
小结
通过自定义指令的方式,可以非常便捷的帮助开发人员做校验的验证,用法也非常的简单,我个人觉得这是一个非常可行的技术方案,要说它最大的问题,无非就是无法有效的利用IView或ElementUI的错误提示,只要能解决这个问题,其实是非常优秀的用法;
方式三:自定义组件/动态表单
这种方式其实相对最合理,二次封装IView或者ElementUI,将所有的类型判断都写进自定义组件里面去,仅通过对外暴露的type就可以限定里面输入的内容;
比如:
<a-input type="number"></a-input>
如果说,这如何有效的利用错误提示,其实一样,不行的话就可以把这个form都重新封装一遍,将类似于IView的FormItem的props一起封装进去,其实到这里完全 可以使用动态表单 实现了,根据Schema直接生成对应的组件;
由于自定义组件其实涉及的东西挺多的这边就不一一赘述了,后续可以关于这部分单独写一篇博客,而且网上有很多大佬都已经分享了各种方案,比如这篇网易云的这篇《面向复杂场景的表单解决方案》
最后
其实,我觉得表单的最优方案应该是动态表单,根据Schema直接生成一个Form表单,同时集成了各种各样的验证规则在里面,这对于前端开发来说是最能提效的开发方式,但是同样动态表单的开发难度也是最大;
另外,动态表单的用法相对比较适用于新项目或者重构项目,如果是对老项目做小范围优化的的话,公共函数或者指令其实都是不错的方案,归根结底还是那句话,没有最好的技术,只有最适用的场景;