ElementUI是我用得很多的一套UI库,而且做前端么,不可避免地要写一些表单;虽然有人说表单会让用户厌烦,但是很多场景就是要让用户填表单。
写了表单就要写表单验证,但ElementUI的表单验证并不是特别友好,会出现大量的逻辑冗余。比如,如果我要验证结束时间不能早于开始时间,同时结束日期不能早于开始日期,需要写两遍极其相似的逻辑:
var validateTime = (rule, value, callback) => {
if (value === '') {
callback(new Error('结束时间不能为空'));
} else if (value <= this.form.startTime) {
callback(new Error('结束时间不能早于开始时间'));
} else {
callback();
}
};
var validateDate = (rule, value, callback) => {
if (value === '') {
callback(new Error('结束日期不能为空'));
} else if (value <= this.form.startDate) {
callback(new Error('结束日期不能早于开始日期'));
} else {
callback();
}
};
可以看到,这里的表单验证逻辑完全相同,只是换了一下验证的数据和提示语。可以想象,如果有多个表单,并且每个表单里都有需要验证的日期和时间,会有大量冗余(事实上这次的业务场景就是这样)。
解决这个问题,主要是对其中的变化点进行封装(EVP),可以抽象出这样一个函数:
function endTimeValidator (
startTime,
noValueMsg = '请指定结束时间',
beforeStartTimeMsg = '结束时间必须晚于开始时间',
required = true
) {
return (rule, value, callback) => {
if (value && value <= startTime) {
callback(new Error(beforeStartTimeMsg))
} else {
if (required && !value) {
callback(new Error(noValueMsg))
} else {
callback()
}
}
}
}
其中required用于指定是否是必选。在具体使用的时候,可以这样:
endTimeValidator(
this.courseAdditionData.courseStartTime,
'请选择课程结束时间',
'课程结束时间必须晚于开始时间',
false
)
但是这个函数存在两个问题:
- Vue的生命周期,在组件created之前data没有初始化,如果直接在data中声明(就像官方文档的用法一样),会报错undefined;
- 虽然Vue的data是可响应的,但函数本身不是,可以看到,调用的时候传入的是data在初始化的时候的快照,这就导致了函数里获取到的事实上一直是
courseStartTime
的初始值。
为了解决第一个问题,可以把这个函数放到mounted里。但是第二个问题呢?这个时候我们可以想到JS的一个语言特性:闭包。所以可以对函数进行改造:
function endTimeValidator (
startTime, // startTime的类型是函数,() => string
noValueMsg = '请指定结束时间',
beforeStartTimeMsg = '结束时间必须晚于开始时间',
required = true
) {
return (rule, value, callback) => {
if (value && value <= startTime()) {
callback(new Error(beforeStartTimeMsg))
} else {
if (required && !value) {
callback(new Error(noValueMsg))
} else {
callback()
}
}
}
}
这样,利用函数闭包,实现了类似于late binding的效果。在使用的时候可以这样:
endTimeValidator(
() => this.courseAdditionData.courseStartTime,
'请选择课程结束时间',
'课程结束时间必须晚于开始时间',
false
)
思路主要来源于Vue 3的函数式API。在早期(还是vue-composition-api的时候),实现大概是这样:
interface Option<T> {
get: () => T;
set: (value: T) => void;
}
export declare function computed<T>(getter: Option<T>['get']): Ref<T>;
也是利用函数闭包来实现的。虽然现在已经是alpha了,但是其实这一块的核心逻辑变化并不大。
再多想一点,从某个层面来说,这个其实和代理模式(proxy pattern)是有点像的。这个函数闭包是作为真实数据的一个代理而存在的,用来解决直接访问数据而导致的不能响应的问题;当然如果直接当成是一个委托关系似乎也没什么问题。说白了,还是为了解耦,让逻辑不要和数据耦合在一起。