最近在开发 uniapp
小程序项目,用到了 uviewUI 组件库,项目里有一些表单应用场景用到了 u-form
组件,有很多需要验证业务开发,所以仔细看了一下组件内部的一些实现,发现用到了 async-validator
,又继续阅读了 async-validator
的源码,受益良多,不禁感叹作者设计精妙。这期我们还是看看如何将 async-validator
应用到 u-form
组件中。
我这里使用的是 uview 1.x
的版本,uview
的组件有详情的中文注释,国人友好,所以基本上没有任何阅读障碍就能看明白。
下面我们主要分析一下其实现的方案
form
组件分成两个组件,一个容器组件 u-form
, 一个表单项组件 u-form-item
u-form 组件
容器组件就是一个 view
节点,没有样式,主要内容在 js
部分
<view class="u-form"><slot /></view>
复制代码
使用
<u-form :model="form" ref="uForm">
<u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
</u-form>
复制代码
props
主要接收四种参数,表单对象、下边框显示、标签样式、错误提示方式 这些数据都被子组件 u-form-item
用到
model 表单数据对象
border-bottom 是否显示表单域的下划线边框
label-position 表单域提示文字的位置,left-左侧,top-上方
label-width 提示文字的宽度,单位rpx(默认90)
label-style lable的样式,对象形式
label-align lable的对齐方式
error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
复制代码
个性化的错误消息提醒设置
error-type
提供多种错误的显示方式,可以同时配置1个或多个
message
:默认为输入框下方用文字进行提示none
:只要包含此值,将不会进行任何提示border-bottom
:配置作用域底部的下划线显示为红色border
:配置输入框的边框为红色进行提示 -- 如果有配置显示Input组件显示边框的话toast
:以"toast"提示的方式弹出错误信息,每次只弹出最前面的那个表单域的错误信息(1.3.5新增)
<u-form :model="form" ref="uForm" :error-type="['message']"></u-form>
复制代码
methods
使用都通过 refs
获取到组件实例调用其内部方法来进行验证等其它操作
// 设置规则
this.$refs.uForm.setRules(rules);
// 重置
this.$refs.uForm.resetFields();
// 检验
this.$refs.uForm.validate((isValid:boolean) => void);
复制代码
u-form-item
u-form-item
组件的职责只要用于布局,显示 label
、icon
、message
和 表单组件
使用
<u-form :model="form" ref="uForm">
<u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
</u-form>
复制代码
基本结构
<view>
<view class="body">
<view class="left">
<view class="left__content">
<text class="content--required">*</text>
<view class="content__icon" v-if="leftIcon"><u-icon/></view>
<view class="content__label">{{label}}</view>
</view>
</view>
<view class="right">
<view class="right__content">
<view class="right__content__slot "><slot /></view>
<view class="right__content__icon">
<u-icon />
<slot name="right" />
</view>
</view>
</view>
</view>
<view class="message">{{validateMessage}}</view>
</view>
复制代码
props
label 左侧提示文字
// 用于验证时,获取字段对应的 rules 规则
prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
// 这几个与 u-form 相同
border-bottom 是否显示表单域的下划线边框
label-position 表单域提示文字的位置,left-左侧,top-上方
label-width 提示文字的宽度,单位rpx(默认90)
label-style lable的样式,对象形式
label-align lable的对齐方式
right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
left-icon-style 左侧图标的样式,对象形式
right-icon-style 右侧图标的样式,对象形式
required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
复制代码
methods
// 用于向下传递验证结果
broadcastInputError() {
this.broadcast('u-input', 'on-form-item-error'}
},
// 监听向上传递的 blur 和 change 事件, 判断是否需要required校验
setRules() {
// blur事件
this.$on('on-form-blur', that.onFieldBlur);
// change事件
this.$on('on-form-change', that.onFieldChange);
},
// 从u-form的rules属性中,取出当前u-form-item的校验规则
getRules() {},
// 过滤出符合要求的rule规则
getFilteredRule() {},
// 校验数据
validation(trigger, callback = () => {}) {},
// 清空当前的u-form-item
resetField() {}
复制代码
初始化 mouted
u-form
的初始化,生成了一个 fields
空数组挂在实例上,用于存储所有 u-form-item
的实例
created() {
// 存储当前form下的所有u-form-item的实例
// 不能定义在data中,否则微信小程序会造成循环引用而报错
this.fields = [];
},
复制代码
u-form-item
初始化
- 获取父级
u-form
的实例,挂到this.parent
上 - 将
u-form
需要传递的props
复制到u-form-item
的parentData
中 - 将自身实例添加
u-form
的fields
中 - 设置错误提醒类型和初始值
- 监听子组件的
blur
与change
事件
// 组件创建完成时,将当前实例保存到u-form中
mounted() {
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
this.parent = this.$u.$parent.call(this, 'u-form');
if (this.parent) {
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData
Object.keys(this.parentData).map(key => {
this.parentData[key] = this.parent[key];
});
// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
if (this.prop) {
// 将本实例添加到父组件中
this.parent.fields.push(this);
this.errorType = this.parent.errorType;
// 设置初始值
this.initialValue = this.fieldValue;
// 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
this.$nextTick(() => {
this.setRules();
})
}
}
},
复制代码
父子组件的数据通信设计
由于 h5
与 小程序
的差异,在小程序中可以通过 this.$parent
准确的获取到父组件,但在h5中可能需要使用 this.$parent.$parent
来获取父组件的实例。uniapp 实例 property 文档
获取 parent 的代码在 libs/function/$parent.js
中
根据组件的 name
去查找父节点,如果不传 name
就查找最顶层的根节点
// 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
// this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
// 这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name
// 值(默认为undefined),就是查找最顶层的$parent
export default function $parent(name = undefined) {
let parent = this.$parent;
// 通过while历遍,这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等,继续上一级寻找
parent = parent.$parent;
} else {
return parent;
}
}
return false;
}
复制代码
再看 u-form-item
的 mounted
中获取 u-form
节点的代码
- 使用
call
绑定了上下文到this
上,向上查找u-form
组件 - 此时,
u-form-item
就拿到了u-form
的实例,可以取到相关数据 - 同时,通过
this.parent.fields.push(this)
又将u-form-item
的实例注册到了u-form
中的fields
属性中
通过这种方式完成了,u-form-item
拿到了 u-form
的实例, u-form
同时也拿到了所有 u-form-item
的实例,父子组件通信通道已经建立。
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
this.parent = this.$u.$parent.call(this, 'u-form');
// 将本实例添加到父组件中
this.parent.fields.push(this);
复制代码
校验逻辑处理
我们一般通过 this.$refs.validate(callback)
进行检验,那么 u-form
组件是如何处理这个校验逻辑的
- 生成一个
Promise
,方便外部进行promise
调用,同时接收callback
参数,支持callback
调用(和async-validator
设计相同)。 - 遍历
this.fields
就也是每一个子组件u-form-item
的实例,调用其内部的methods
方法validation
来进行每一个字段的规则校验 - 收集每个字段的异常消息到
errorArr
中,所有字段校验完成后,返回promise<boolean>
- 判断是否显示
toast
,调用callback(valid:boolean)
// 校验全部数据
validate(callback) {
return new Promise(resolve => {
// 对所有的u-form-item进行校验
let valid = true; // 默认通过
let count = 0; // 用于标记是否检查完毕
let errorArr = []; // 存放错误信息
this.fields.map(field => {
// 调用每一个u-form-item实例的validation的校验方法
field.validation('', error => {
// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
if (error) {
valid = false;
errorArr.push(error);
}
// 当历遍了所有的u-form-item时,调用promise的then方法
if (++count === this.fields.length) {
resolve(valid); // 进入promise的then方法
// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
this.$u.toast(errorArr[0]);
}
// 调用回调方法
if (typeof callback == 'function') callback(valid);
}
});
});
});
}
复制代码
u-form-item
的 validation
方法
- 通过
this.parent.model
获取u-form
的props
中绑定的model
数据 - 由
u-form-item
的props
中的prop
来获取model
对应字段的值 - 从
u-form
中获取校验规则this.parent.rules[this.prop]
- 调用
async-validator
的方法进行当前字段的校验 - 标记状态和错误,并调用
callback
将异常返回给u-form
// 校验数据
validation(trigger, callback = () => {}) {
// 检验之间,先获取需要校验的值
this.fieldValue = this.parent.model[this.prop];
// blur和change是否有当前方式的校验规则
let rules = this.getFilteredRule(trigger);
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
// 对count变量的统计错误而无法进入上一层的回调
if (!rules || rules.length === 0) {
return callback('');
}
// 设置当前的装填,标识为校验中
this.validateState = 'validating';
// 调用async-validator的方法
let validator = new schema({
[this.prop]: rules
});
validator.validate({
[this.prop]: this.fieldValue
}, {
firstFields: true
}, (errors, fields) => {
// 记录状态和报错信息
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
// 调用回调方法
callback(this.validateMessage);
});
},
复制代码
事件的传递设计
rules 中支持 trigger
配置,用于配置使用什么方式来触发校验
- 当输入框获取焦点或输入内容发生变化时触发校验
- 当校验失败时,输入框要改变样式,如红色边框
error-type
提供的错误提示方式中,toast/message/border-bottom
这几种都是在 u-form-item
中控制的,border
是在子组件如 u-input
组件中控制的。
实现跨组件的消息通信,uview
通过事件总线的方式来实现上面的功能
rules: {
name: [
// 对name字段进行长度验证
{
min: 5,
message: '简介不能少于5个字',
trigger: 'change'
},
// 对name字段进行必填验证
{
required: true,
message: '请填写姓名',
trigger: ['change','blur']
},
]
}
复制代码
- 在
u-form-item
中通过监听子组件向上传递的blur
和change
事件,来触发校验事件 - 校验完成后,通过向下传递
on-form-item-error
事件 通知子组件去更新状态显示错误提示
broadcastInputError() {
// 子组件发出事件,第三个参数为true或者false,true代表有错误
this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
},
// 判断是否需要required校验
setRules() {
let that = this;
// blur事件
this.$on('on-form-blur', that.onFieldBlur);
// change事件
this.$on('on-form-change', that.onFieldChange);
},
复制代码
由于不能保证 u-form-item
和 u-input
组件之间是否还有其它节点,这需要事件有目标的一层层向下或向下传递
uview 事件的派发和广播封装在 libs/util/emitter.js
中
- 向下传递时,递归向下查找与
componentName
对应的组件,找到后,通过组件.$emit.apply
来触发事件并传递参数 - 向上传递时,同样通过
while
循环(类递归)的方式向上查找与componentName
对应的组件,同样通过$emit
来触发事件
function broadcast(componentName, eventName, params) {
// 循环子节点找到名称一样的子节点 否则 递归 当前子节点
this.$children.map(child=>{
if (componentName===child.$options.name) {
child.$emit.apply(child,[eventName].concat(params))
}else {
broadcast.apply(child,[componentName,eventName].concat(params))
}
})
}
export default {
methods: {
/**
* 派发 (向上查找) (一个)
* @param componentName // 需要找的组件的名称
* @param eventName // 事件名称
* @param params // 需要传递的参数
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;//$parent 找到最近的父节点 $root 根节点
let name = parent.$options.name; // 获取当前组件实例的name
// 如果当前有节点 && 当前没名称 且 当前名称等于需要传进来的名称的时候就去查找当前的节点
// 循环出当前名称的一样的组件实例
while (parent && (!name||name!==componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
// 有节点表示当前找到了name一样的实例
if (parent) {
parent.$emit.apply(parent,[eventName].concat(params))
}
},
/**
* 广播 (向下查找) (广播多个)
* @param componentName // 需要找的组件的名称
* @param eventName // 事件名称
* @param params // 需要传递的参数
*/
broadcast(componentName, eventName, params) {
broadcast.call(this,componentName, eventName, params)
}
}
}
复制代码
总结
通过对源码的学习,对 u-form
有了更全面的认识,后面在相关业务的开发中会更加得心应手。 应用 async-validator
也大大简化了表单验证的代码逻辑,相关源码请查看 uview1.8.6
同时,也发现了源码在实现上的一些问题:
blur
和change
事件只能用在u-input
组件上,- 错误提示和禁用没有去更新
input
、checkbox
、radio
的样式, u-form
没有提供验证单个field
的方法