async-validator 源码解析(三) - 组件应用

最近在开发 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(默认90label-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 组件的职责只要用于布局,显示 labeliconmessage 和 表单组件

使用

<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(默认90label-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 初始化

  1. 获取父级 u-form 的实例,挂到 this.parent
  2. u-form 需要传递的 props 复制到 u-form-itemparentData
  3. 将自身实例添加 u-formfields
  4. 设置错误提醒类型和初始值
  5. 监听子组件的 blurchange 事件
// 组件创建完成时,将当前实例保存到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-itemmounted 中获取 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 组件是如何处理这个校验逻辑的

  1. 生成一个 Promise,方便外部进行 promise 调用,同时接收 callback 参数,支持 callback 调用(和 async-validator 设计相同)。
  2. 遍历 this.fields 就也是每一个子组件 u-form-item 的实例,调用其内部的 methods 方法 validation 来进行每一个字段的规则校验
  3. 收集每个字段的异常消息到 errorArr 中,所有字段校验完成后,返回 promise<boolean>
  4. 判断是否显示 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-itemvalidation 方法

  1. 通过 this.parent.model 获取 u-formprops 中绑定的 model 数据
  2. u-form-itemprops 中的 prop 来获取 model 对应字段的值
  3. u-form 中获取校验规则 this.parent.rules[this.prop]
  4. 调用 async-validator 的方法进行当前字段的校验
  5. 标记状态和错误,并调用 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 中通过监听子组件向上传递的 blurchange 事件,来触发校验事件
  • 校验完成后,通过向下传递 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-itemu-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

同时,也发现了源码在实现上的一些问题:

  • blurchange 事件只能用在 u-input 组件上,
  • 错误提示和禁用没有去更新 inputcheckboxradio 的样式,
  • u-form 没有提供验证单个 field 的方法

猜你喜欢

转载自juejin.im/post/7127858439274889247
今日推荐