在 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 ( ) { 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
对象:
class Validator { _validators = [] _errorMsg = [] add (value, rules ) { for (let rule of rules) { let {validator, errorMsg='' , params=[]} = rule; this ._validators.push(() => validator.call(null , value, ...params)); this ._errorMsg.push(errorMsg); } return this ; } 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())
如上述代码,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' ; console .log(obj.key);
这样,我们可以创建一个校验器对象,然后使用表单的元素为其赋值的时候便会对其进行校验,这次表单校验函数的大体结构如下:
function validate ( ) { let validators = { name : strategies, age : strategies, email : strategies }; let formObj = validatorCreater({}, validators); 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 : '邮箱格式不正确' }] };
接下来我们实现 validatorCreater
,validatorCreater
是在目标对象上创建一个有检验功能的代理:
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); 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); }
参考