文章目录
- 场景
- 思考
- Mixin
- 具体代码
- 目录结构
- validator相关代码
- FormItem相关
- 按需引用
- 使用
- 总结
场景
ElementUI
的Form
表单组件自带的校验规则是不是有点少,通过yarn.lock
查询ElementUI
得知校验使用了async-validator
依赖
阅读async-validator
相关文档得知内置类型如下:
- string: 必须是 string,默认类型;
- number: 必须是 number;
- boolean: 必须是 boolean;
- method: 必须是 function;
- regexp: 必须是正则或者是在调用 new RegExp 时不报错的字符串;
- integer: 必须是number类型且为整数;
- float: 必须是number类型且为浮点数;
- array: 必须是数组,通过 Array.isArray 判断;
- object: 必须是对象且不为数组;
- enum: 值必须出现在 enum 枚举值中;
- date: 合法的日期,使用 Date 对象判断;
- url: url类型;
- hex: 16进制;
- email: 邮箱地址;
常用类型有email
,却没phone
,如何自定义规则类型呢,比如ip
、mac
、phone
,且达到同样的效果。
写法如下:
computed: {
rules() {
const required = true;
return {
email: [{ required, type: 'email', message: '请输入正确的邮箱' }],
phone: [{ required, type: 'phone', message: '请输入正确的手机号码' }],
url: [{ required, type: 'url', message: '请输入正确的url' }],
ip: [{ required, type: 'ip', message: '请输入正确的IP地址' }],
mac: [{ required, type: 'mac', message: '请输入正确的MAC地址' }]
}
}
},
实现效果如下:(左:失败、右:成功)
思考
通过阅读ElementUI
的 FormItem
组件源码,FormItem
组件validate
函数引用了async-validator
,具体到下面29
行代码,它初始化了一个校验器,若我们在初始化之前改造AsyncValidator
函数是不是就能实现自定义类型扩展?FormItem
核心源码解读:
// 路径 node_modules\element-ui\packages\form\src\form-item.vue
import AsyncValidator from 'async-validator';
export default {
methods: {
// 核心校验函数 trigger值为change、blur 默认change
validate(trigger, callback = noop) {
// 开放校验
this.validateDisabled = false;
// 过滤对应trigger的规则
const rules = this.getFilteredRule(trigger);
// 若无规则或无必填 回调并返回
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
// 设置当前校验状态:校验中
this.validateState = 'validating';
// 以下代码可参考async-validator使用文档
const descriptor = {};
// 因为async-validator的rules没有trigger属性 删除每条规则的trigger
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
// 以FormItem当前prop属性为键赋值对应rules
descriptor[this.prop] = rules;
// 以descriptor为构造参数 初始化一个校验器
const validator = new AsyncValidator(descriptor);
const model = {};
// 获取当前FormItem的对应字段值
model[this.prop] = this.fieldValue;
// 触发表单校验 firstFields: true 指当某一规则校验失败时,终止校验;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
// 回调校验信息、不符合的字段
callback(this.validateMessage, invalidFields);
// 对应 Form Events 如果elForm存在,则每次触发validate事件
// 参数:被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
}
}
通过阅读asycn-validator
官方文档,使用例子如下,是不是很熟悉,FormItem
组件validate
函数照着官方例子进一步封装,下面的Schema
也就是上面的AsyncValidator
,只要弄懂Schema
大概便能实现我们要的效果。
import Schema from 'async-validator';
const descriptor = {
// name字段的校验规则
name: {type: "string", required: true},
};
const validator = new Schema(descriptor);
validator.validate({ name: "muji" }, (errors, fields) => {
if (errors) {
// 校验不通过 do something
return;
}
// 校验通过 do something
});
通过阅读asycn-validator
源码,Schema
类开放了一个register
注册器,不过官方文档并没有这个API的介绍,推测是注册校验规则类型。
// 路径node_modules\async-validator\es\index.js
Schema.register = function register(type, validator) {
if (typeof validator !== 'function') {
throw new Error('Cannot register a validator by type, validator is not a function');
}
validators[type] = validator;
};
追溯到validators
,这里存放了内置类型,验证了刚才的设想,我们在new AsyncValidator(descriptor)
之前注册自己想要的类型,便可扩展。
我们拎个默认类型string
的源码观摩一下:
// node_modules\async-validator\es\validator\string.js
import rules from '../rule/';
import { isEmptyValue } from '../util';
/**
* 对string类型校验.
*
* @param rule 校验规则.
* @param value 源对象字段的值.
* @param callback 回调函数.
* @param source 校验后的源对象.
* @param options 校验选项.
* @param options.messages 校验信息.
*/
function string(rule, value, callback, source, options) {
var errors = [];
var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
if (validate) {
// 如果表单值为空 且不是必填 不校验
if (isEmptyValue(value, 'string') && !rule.required) {
return callback();
}
// 校验必填
rules.required(rule, value, source, errors, options, 'string');
if (!isEmptyValue(value, 'string')) {
// 以下代码为校验逻辑 可换成自定义业务校验
// 校验值类型
rules.type(rule, value, source, errors, options);
// 校验是否满足区间
rules.range(rule, value, source, errors, options);
// 校验正则
rules.pattern(rule, value, source, errors, options);
if (rule.whitespace === true) {
// 校验空白字符
rules.whitespace(rule, value, source, errors, options);
}
}
}
callback(errors);
}
export default string;
如果我们扩展一个phone
类型,就要实现类似上面这个string
校验函数,
上面26
到36
行代码可换成我们需要的校验函数,通过register
函数注册,便注册了一个phone
类型。
校验函数代码如下:
function validator(rule, value, callback, source, options) {
const errors = [];
// 如果表单值为空 且不是必填 不校验
if (isEmptyValue(value, 'string') && !rule.required) {
return callback();
}
// 校验必填
rules.required(rule, value, source, errors, options);
// 手机号校验
if (!isEmptyValue(value, 'string') && !/^1\d{10}$/.test(value)) {
errors.push('phone格式不正确');
}
return callback(errors);
}
上面10
到12
代码是会根据业务变化的的,其余代码都是不变的,我们将校验业务抽出封装一下。
代码如下(上下对比一下):
function validator(rule, value, callback, source, options) {
const type = rule.type;
const errors = [];
// 如果表单值为空 且不是必填 不校验
if (isEmptyValue(value, 'string') && !rule.required) {
return callback();
}
// 校验必填
rules.required(rule, value, source, errors, options);
// 自定义规则校验 ruleDict存放各种自定义的校验规则
if (!isEmptyValue(value, 'string') && !ruleDict[type](value)) {
errors.push(format('%s 格式不正确', rule.fullField));
}
return callback(errors);
}
ruleDict
存放各个类型的校验,比如port
、phone
等等。
部分代码如下:
// ruleDict校验规则
export default {
// 端口号
port(value) {
return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
},
// 手机号
phone(value) {
return /^1\d{10}$/.test(value);
}
};
统一进行register
,并将 Schema
吐出。
// 注册validator
Object.keys(ruleDict).forEach(key => {
Schema.register(key, validator);
});
export default Schema;
Schema
改造好了,那FormItem
组件怎么引用呢?这是就想到了我们把源码整个copy
一份不就好了?
这是一种办法,不过还有更好的,本文主角——混入(mixin)
;(总是最后出场)。
Mixin
混入 mixin
是Vue自带的,用来复用Vue组件一种灵活方式。mixin
对象可以包含任意组件选项,常见选项props、data、components、methods 、computed等等,还有常见的生命周期钩子
比如created、mounted。
不仅mixin
对象和组件之间互不影响,而且当组件和mixin
对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
精简混入代码如下:
import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
// 引入扩展后的校验器
import AsyncValidator from './validator/index';
export default {
name: 'ElFormItem',
// 混入一个FormItem组件
mixins: [FormItem],
methods: {
// 核心校验函数 trigger值为change、blur 默认change
validate(trigger, callback = noop) {
// 省略其余代码
const validator = new AsyncValidator(descriptor);
// 省略其余代码
}
}
};
如上代码,只要混入了FormItem
组件,即可拥有其全部组件选项,即复制版FormItem
组件,再重写methods
的validate
函数即可引入我们扩展后的AsyncValidator
。
这么实现的好处:
- 不用拷贝整个代码;
- 组件和复用组件之间互不影响,并以恰当方式合并;
- ElementUI组件升级,最多影响
validate
这一个函数;
最后再按需引用FormItem
组件:
// 引入mixin后的FormItem组件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);
具体代码
目录结构
|-- components # 组件
| |-- Mixins # 混用组件
| | |-- FormItem
| | | |-- index.js # mixin后的ElementUI FormItem组件
| | | |-- validator # async-validator 校验器相关
| | | |-- index.js # 注册自定义组件
| | | |-- rules.js # 自定义校验规则
validator相关代码
// src\components\Mixins\FormItem\validator\rules.js
const patterns = {
ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
mac: /^(([a-fA-F0-9]{2}[:-]?)){5}[a-fA-F0-9]{2}$/
};
export default {
ip(value) {
return patterns.ip.test(value);
},
port(value) {
return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
},
mac(value) {
return patterns.mac.test(value);
},
phone(value) {
return /^1\d{10}$/.test(value);
}
};
// src\components\Mixins\FormItem\validator\index.js
import Schema from 'async-validator';
import rules from 'async-validator/lib/rule';
import { format, isEmptyValue } from 'async-validator/lib/util';
// 规则列表
import ruleList from './rules';
/**
* @param rule 校验规则.
* @param value 源对象字段的值.
* @param callback 回调函数.
* @param source 校验后的源对象.
* @param options 校验选项.
* @param options.messages 校验信息.
*/
function validator(rule, value, callback, source, options) {
const type = rule.type;
const errors = [];
// 如果表单值为空 且不是必填 不校验
if (isEmptyValue(value, 'string') && !rule.required) {
return callback();
}
// 校验必填
rules.required(rule, value, source, errors, options);
// 自定义规则校验
if (!isEmptyValue(value, 'string') && !ruleList[type](value)) {
errors.push(format('%s 格式不正确', rule.fullField));
}
return callback(errors);
}
// 注册validator
Object.keys(ruleList).forEach(key => {
Schema.register(key, validator);
});
export default Schema;
FormItem相关
// src\components\Mixins\FormItem\index.js
import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
import AsyncValidator from './validator/index';
export default {
name: 'ElFormItem',
mixins: [FormItem],
methods: {
// 核心校验函数 trigger值为change、blur 默认change
validate(trigger, callback = noop) {
// 开放校验
this.validateDisabled = false;
// 过滤对应trigger的规则
const rules = this.getFilteredRule(trigger);
// 若无规则或无必填 回调并返回
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
// 设置当前校验状态:校验中
this.validateState = 'validating';
// 以下代码可参考async-validator使用文档
const descriptor = {};
// 因为async-validator的rules没有trigger属性 删除每条规则的trigger
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
// 以FormItem当前prop属性为键赋值对应rules
descriptor[this.prop] = rules;
// 以descriptor为构造参数 初始化一个校验器
const validator = new AsyncValidator(descriptor);
const model = {};
// 获取当前FormItem的对应字段值
model[this.prop] = this.fieldValue;
// 触发表单校验 firstFields: true 指当某一规则校验失败时,终止校验;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
// 回调校验信息、不符合的字段
callback(this.validateMessage, invalidFields);
// 对应 Form Events 如果elForm存在,则每次触发validate事件
// 参数:被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
}
}
};
按需引用
在main.js里按需引用
// 引入mixin后的FormItem组件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);
使用
写法还是和原来一样如下:
<template>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="ip" prop="ip">
<el-input v-model="form.ip" />
</el-form-item>
<el-form-item label="MAC" prop="mac">
<el-input v-model="form.mac" />
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
form: {}
}
},
computed: {
rules() {
const required = true;
return {
ip: [{ required, type: 'ip', message: '请输入正确的IP地址', trigger: 'blur' }],
mac: [{ required, type: 'mac', message: '请输入正确的MAC地址', trigger: 'blur' }]
}
}
},
}
</script>
写法还是和原来一样,却多了自定义校验规则,岂不美哉。
总结
通过源码解读,一步一步摸索出来的,新人创作不易,求点赞关注给点动力。