超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

前端表单校验的设计思考

在 Web 开发中经常需要面临表单校验的问题,通常需要前后端结合设计,本文就前端表单校验的模式进行简单的探讨。

常规的 if…else

最常见的关于表单校验的方式就是 if...else 的嵌套了吧,比如我们有个表单有三个文本域:姓名、年龄和邮箱:

<div class='form'>
<div class="form-item">
<div class='label'>
<span>用户名:</span>
<input id='name' name='用户名' type="text" />
</div>
</div>

<div class="form-item" >
<div class='label'>
<span>年龄:</span>
<input id='age' name='年龄' type="text" />
</div>
</div>

<div class="form-item" >
<div class='label'>
<span>邮箱:</span>
<input id='mail' name='邮箱' type="text" />
</div>
</div>
</div>

<button id='submit' >提交</button>

我们需要对其进行校验,要求所有字段不能为空,年龄必须是数字切位于 0-100 之间:

const isNotEmpty = value => value !== '';

const isNumber = value => /^[0-9]*$/.test(value);

const isBetween = (value, min, max) => {
if (max === undefined) {
max = Number.MAX_VALUE;
}
if (min === undefined) {
min = Number.MIN_VALUE;
}
return value > min && value < max;
}

const isEmail = value => {console.log(value);return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value);}

document.getElementById('submit').addEventListener('click', validate);

function validate() {
let eleName = document.getElementById('name');
let eleAge = document.getElementById('age');
let eleMail = document.getElementById('mail');
if (!isNotEmpty(eleName.value)) {
alert('姓名必须不为空');
return false;
}
if (!isNumber(eleAge.value)) {
alert('年龄必须为数字');
return false;
}
if (!isBetween(eleAge.value, 0, 100)) {
alert('年龄必须为数字');
return false;
}
if (!isNotEmpty(eleMail.value)) {
alert('邮箱不能为空');
return false;
}
if (!isNumber(eleMail.value)) {
alert('邮箱格式不正确');
return false;
}
};

这个代码很明显,包含一大堆 if...else 语句,看起来啰里啰嗦的,如果修改规则还需要深入代码内部进行修改;复用性极差,如果还有一个表单还需要重新复制一大堆校验代码。

策略模式

了解设计模式的话可以很容易想到策略模式,策略模式非常适合改写一大串的 if...else 语句。

通过策略模式该写的校验函数,结构十分清晰:

function validate() {
// 获取form元素
let ele = document.getElementById('formEle');

// 创建表单校验实例
let validator = new Validator();

// 编写校验配置
validator.add(ele.value, strategies);

// 开始校验,并接收错误信息
let errorMsg = validator.validate()

// 如果有错误信息输出,说明校验未通过
if(errorMsg !== true){
alert(errorMsg)
return false//阻止表单提交
}
}

这样流程明显清晰多了,也方便复用,我们来看一下怎么实现

首先定义我们的添加校验规则的接口,对于一个值可能有多个校验函数,因此希望 strategies 是个数组,数组成员包含校验函数 validator、校验失败的错误信息 errorMsg、部分校验函数可能需要的额外参数 params

validator.add(ele.value, [{
validator: function,
errorMsg: string,
params: [parmas]
}, ...]);

然后就是执行的校验函数:

validator.validate();

这里我们开始实现 Validator 对象:

class Validator {
// 缓存校验函数
_validators = []
// 缓存错误消息
_errorMsg = []

add(value, rules) {
for (let rule of rules) {
let {validator, errorMsg='', params=[]} = rule;
// 使用 call 绑定校验函数所需的参数
this._validators.push(() => validator.call(null, value, ...params));
this._errorMsg.push(errorMsg);
}
return this; // 支持链式调用 validator.add().add().add()...
}

validate() {
for (let validateIndex in this._validators) {
let result = this._validators[validateIndex]();
if (!result) {
return this._errorMsg[validateIndex];
}
}
return true;
}
}

调用代码:

function validate(){
let eleName = document.getElementById('name');
let eleAge = document.getElementById('age');
let eleMail = document.getElementById('mail');

let validator = new Validator();
validator.add(eleName.value, [{
validator: isNotEmpty,
errorMsg: '姓名必须不为空'
}]).add(eleAge.value, [{
validator: isNumber,
errorMsg: '年龄必须为数字'
}, {
validator: isBetween,
errorMsg: '年龄必须为大于 0 并且小于 100',
params: [0, 100]
}]).add(eleMail.value, [{
validator: isNotEmpty,
errorMsg: '邮箱不能为空'
}, {
validator: isEmail,
errorMsg: '邮箱格式不正确'
}])

var result = validator.validate();
if(result){
alert(result);
return false;
}

alert('验证通过');
}

全部代码在线示例:Code Pen

上面这种使用了组合、委托等思想,可以避免多种条件选择语句;同时将算法封装在独立的 strategy 中,使得它易于切换,易于理解,易于拓展。

但是提前绑定了要检验的值,当要校验的对象的值类型为非引用数值的时候,被校验的对象不会随之变更:

let obj = {
name: '',
}

let validator = new Validator().add(obj.name, [{
validator: isNotEmpty,
errorMsg: '姓名必须不为空'
}])

console.log(validator.validate())
// "姓名必须不为空"

obj.name = "Jack";
console.log(validator.validate())
// 期望为“true” 实际为“姓名必须不为空”

如上述代码,obj.name 已经被固定为 ''obj.name 修改时并不能同步校验函数中的值,下一节提到的方案将会解决这个问题。

Proxy

Proxy 是 javascript 提供的用于在语言层面修改一些操作的默认行为,当然在读写值的时候增加一层校验非常方便.

这是一个修改对象默认读写值方法的示例:

let obj = new Proxy({}, {
get (target, key, receiver) {
console.log(`"${key}" getter`);
return Reflect.get(target, key, receiver);
},

set (target, key, value, receiver) {
console.log(`"${key}" setter`);
return Reflect.set(target, key, value, receiver);
}
});

obj.key = 'value';
// "key" setter
console.log(obj.key);
// "key" getter
// value

这样,我们可以创建一个校验器对象,然后使用表单的元素为其赋值的时候便会对其进行校验,这次表单校验函数的大体结构如下:

function validate() {
let validators = {
name: strategies,
age: strategies,
email: strategies
};

// 创建表单校验对象实例
let formObj = validatorCreater({}, validators);

// 获取form元素
let ele = document.getElementById('formEle');

// 开始校验,并处理失败信息
try {
formObj.key = ele.value
} catch(e) {
alert(e);
return false;
}
return true;
}

我们来逐个实现它的每一部分,首先是 validators 为了灵活,我们定义成类似于上一节的结构:

let validators = {
name: [{
validator: isNotEmpty,
errorMsg: '姓名必须不为空'
}],
age: [{
validator: isNumber,
errorMsg: '年龄必须为数字'
}, {
validator: isBetween,
errorMsg: '年龄必须为大于 0 并且小于 100',
params: [0, 100]
}],
email: [{
validator: isNotEmpty,
errorMsg: '邮箱不能为空'
}, {
validator: isEmail,
errorMsg: '邮箱格式不正确'
}]
};

接下来我们实现 validatorCreatervalidatorCreater 是在目标对象上创建一个有检验功能的代理:

var validatorCreater = (target, validator) => new Proxy(target, {
// 保存校验器
_validator: validator,
set(target, key, value, receiver) {
// 如果赋值的属性存在校验器,则进行校验
if (this._validator[key]) {
// 遍历其多个子校验器
for (validatorStrategy of this._validator[key]){
let {validator, errorMsg='', params=[]} = validatorStrategy;
if (!validator.call(null, value, ...params)) {
// 校验失败抛出异常
throw new Error(errorMsg);
return false;
}
}
}
// 赋值语句放最后,如果失败不赋值,如果不存在校验器则赋值
return Reflect.set(target, key, value, receiver);
}
});

这里说一下,setter 函数内部只能返回 true 或者 false,而系统内部消化了这个返回的布尔值结果,也就是说这个返回的 true 或者 false 我们是无法看的到,所以当校验失败的时候我们选择抛出异常处理更清晰。

提交表单的校验函数时用 try...catch 检查是否赋值成功:

function validate() {
// 创建表单校验对象实例
let formObj = validatorCreater({}, validators);

// 获取form元素
let eleName = document.getElementById('name');
let eleAge = document.getElementById('age');
let eleMail = document.getElementById('mail');

// 开始校验,并接收错误信息
try {
formObj.name = eleName.value;
formObj.age = eleAge.value;
formObj.email = eleMail.value;
} catch (e) {
alert(e.message);
return false;
}
alert('验证通过');
return true;
}

全部代码在线示例:Code Pen

如果熟悉 Object.defineProperty 会想到 Object.defineProperty 也可以修改对象的 setter ,当然,也可以使用 Object.defineProperties 进行校验:

var validatorCreater = (target, validator) => {
let obj = {};

let descriptors = {};

for (let key in validator) {
descriptors[key] = {
set(value) {
for (validatorStrategy of validator[key]){
let {validator, errorMsg='', params=[]} = validatorStrategy;
if (!validator.call(null, value, ...params)) {
// 校验失败抛出异常
throw new Error(errorMsg);
return false;
}
}
this._value = value;
return true;
},
get() {
return this._value;
}
}
}

Object.defineProperties(obj, descriptors);
}

参考