iView 是TalkingData 基于 Vue.js 开发的一套高质量 UI 组件库 ,其包含了工程构建、主题定制、多语言等功能,极大提升了我们在开发项目时的工作效率,几个项目下来感觉还是挺好用的,值得安利和深入研究,接下来我们 研究一下其源码,以此来加深对框架的了解。
Form组件结构
通过查看iview源码得知, 一个完整的Form表单主要由Form、 FormItem 主一些表单空件按钮等组成。
--node_modules/iview/src/components/--form.vue--form-item.vue--index.js
其中 :
-
Form 主要是对form做一层封装
-
FormItem 是一个容器,主要用来存放一些表单控件和 标签,提示消息等内容。
源码分析
通常来说,在构建一个form组件时一般都围绕以下几个核心功能展开:
-
收集用户录入的一些数据
-
录入数据校验(自定义校验规则和提示)
-
表单提交校验
iview Form亦是如此,具体如下所示
1.form.vue
//-其实就是为了方便使用Vue.use去初始化这个框架(可以认为是插件,通过install集中管理拆分的组件,//提高iview的可拓展性)import Form from './form.vue';import FormItem from './form-item.vue';Form.Item = FormItem;export default Form;//form.vue<template> <form :class="classes" :autocomplete="autocomplete"><slot></slot></form></template><script> import { oneOf } from '../../utils/assist'; //工具类 const prefixCls = 'ivu-form'; //组件dom前缀 export default { name: 'iForm', props: { model: { type: Object //表单数据对象 }, rules: { type: Object //表单数据对象 }, labelWidth: { type: Number // 表单域标签的宽度,所有的 FormItem 都会继承 Form 组件的 label-width 的值 labelPosition: { //表单域标签的位置,validator是prop中用来校验传入值的自定义函数,返回值boolean validator (value) { return oneOf(value, ['left', 'right', 'top']); default: 'right' }, inline: { //是否开启行内表单模式 type: Boolean, default: false }, showMessage: { //是否显示校验错误信息 type: Boolean, default: true }, autocomplete: { //原生的 autocomplete 属性 validator (value) { return oneOf(value, ['on', 'off']); }, default: 'off' } }, provide() { //provide 可以理解为组件中的一个全局变量,在此处定义的变量 可以供子组件中通过inject使用 return { form : this }; }, data () { return { fields: [] }; }, computed: { classes () { //动态设置表单域标签的位置 return [ `${prefixCls}`, `${prefixCls}-label-${this.labelPosition}`, { [`${prefixCls}-inline`]: this.inline } ]; } }, methods: { //表单重置 即reset resetFields() { this.fields.forEach(field => { field.resetField(); }); }, //表单全局校验,注意返回的是个promise,并没有执行 //this.$refs["xxxForm"].validate(valid => {}) validate(callback) { return new Promise(resolve => { let valid = true; let count = 0; this.fields.forEach(field => { field.validate('', errors => { if (errors) { valid = false; } if (++count === this.fields.length) { // all finish resolve(valid); if (typeof callback === 'function') { callback(valid); } } }); }); }); }, //针对单个 validateField(prop, cb) { const field = this.fields.filter(field => field.prop === prop)[0]; if (!field) { throw new Error('[iView warn]: must call validateField with valid prop string!'); } field.validate('', cb); } }, watch: { //监听定义的rules,更新validate, rules() { this.validate(); } }, created () { //通过$on监听校验字段的变动 //主要是根据FormItem定义的prop来收集需要校验的字段, this.$on('on-form-item-add', (field) => { if (field) this.fields.push(field); return false; }); //同上,移除不需要校验的字段 this.$on('on-form-item-remove', (field) => { if (field.prop) this.fields.splice(this.fields.indexOf(field), 1); return false; }); } };</script>
2.form-item.vue
<template> <div :class="classes"> <label :class="[prefixCls + '-label']" :for="labelFor" :style="labelStyles" v-if="label || $slots.label"><slot name="label">{{ label }}</slot></label> <div :class="[prefixCls + '-content']" :style="contentStyles"> <slot></slot> <transition name="fade"> <div :class="[prefixCls + '-error-tip']" v-if="validateState === 'error' && showMessage && form.showMessage">{{ validateMessage }}</div> </transition> </div> </div></template><script> import AsyncValidator from 'async-validator'; import Emitter from '../../mixins/emitter'; const prefixCls = 'ivu-form-item'; //封装一个通过属性路径获取属性值的方法, // 如:var obj = {name:‘objname‘, items:[{value: 0},{value: 1}]}, path = ‘items.0.value‘; // console.log( getPropByPath(obj, path).v ); // 结果:0 function getPropByPath(obj, path) { let tempObj = obj; path = path.replace(/[(w+)]/g, '.$1'); path = path.replace(/^./, ''); let keyArr = path.split('.'); let i = 0; for (let len = keyArr.length; i < len - 1; ++i) { let key = keyArr[i]; if (key in tempObj) { tempObj = tempObj[key]; } else { throw new Error('[iView warn]: please transfer a valid prop path to form item!'); } } return { o: tempObj, k: keyArr[i], v: tempObj[keyArr[i]] }; } export default { name: 'FormItem', mixins: [ Emitter ], props: { //一些props值 API有具体说明 label: { type: String, default: '' }, labelWidth: { type: Number }, prop: { type: String }, required: { type: Boolean, default: false }, rules: { type: [Object, Array] }, error: { type: String }, validateStatus: { type: Boolean }, showMessage: { type: Boolean, default: true }, labelFor: { type: String } }, data () { return { prefixCls: prefixCls, isRequired: false, validateState: '', validateMessage: '', validateDisabled: false, validator: {} }; }, watch: { error (val) { this.validateMessage = val; this.validateState = val === '' ? '' : 'error'; }, validateStatus (val) { this.validateState = val; }, rules (){ this.setRules(); } }, inject: ['form'], //注入父组件中的变量方便使用 computed: { //样式类。。。 classes () { return [ `${prefixCls}`, { [`${prefixCls}-required`]: this.required || this.isRequired, [`${prefixCls}-error`]: this.validateState === 'error', [`${prefixCls}-validating`]: this.validateState === 'validating' } ]; }, // form() { // let parent = this.$parent; // while (parent.$options.name !== 'iForm') { // parent = parent.$parent; // } // return parent; // }, fieldValue: { cache: false, get() { const model = this.form.model; if (!model || !this.prop) { return; } let path = this.prop; if (path.indexOf(':') !== -1) { path = path.replace(/:/, '.'); } return getPropByPath(model, path).v; //根据prop获取model中的值 } }, //样式类。。。 labelStyles () { let style = {}; const labelWidth = this.labelWidth === 0 || this.labelWidth ? this.labelWidth : this.form.labelWidth; if (labelWidth || labelWidth === 0) { style.width = `${labelWidth}px`; } return style; }, //样式类。。。 contentStyles () { let style = {}; const labelWidth = this.labelWidth === 0 || this.labelWidth ? this.labelWidth : this.form.labelWidth; if (labelWidth || labelWidth === 0) { style.marginLeft = `${labelWidth}px`; } return style; } }, methods: { //设置规则 setRules() { let rules = this.getRules(); if (rules.length&&this.required) { return; }else if (rules.length) { rules.every((rule) => { this.isRequired = rule.required; }); }else if (this.required){ this.isRequired = this.required; } //防止重复定义 this.$off('on-form-blur', this.onFieldBlur); this.$off('on-form-change', this.onFieldChange); this.$on('on-form-blur', this.onFieldBlur); this.$on('on-form-change', this.onFieldChange); }, //汇总rules getRules () { let formRules = this.form.rules; const selfRules = this.rules; formRules = formRules ? formRules[this.prop] : []; return [].concat(selfRules || formRules || []); }, // 根据trigger获取过滤过的规则 如change, blur .... getFilteredRule (trigger) { const rules = this.getRules(); return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1); }, //核心校验方法 validate(trigger, callback = function () {}) { let rules = this.getFilteredRule(trigger); if (!rules || rules.length === 0) { if (!this.required) { //没有定义任何规则的话直接执行后续逻辑 callback(); return true; }else { rules = [{required: true}]; } } this.validateState = 'validating';//构建AsyncValidator参数 let descriptor = {}; descriptor[this.prop] = rules; const validator = new AsyncValidator(descriptor); //底层还是调用的async-validator let model = {}; model[this.prop] = this.fieldValue; // firstField 如果当前字段校验错误就不再继续向下进行验证 validator.validate(model, { firstFields: true }, errors => { this.validateState = !errors ? 'success' : 'error'; //状态 用来判断校验信息是否显示 this.validateMessage = errors ? errors[0].message : ''; //信息 callback(this.validateMessage); }); this.validateDisabled = false; }, //初始化属性值 resetField () { this.validateState = ''; this.validateMessage = ''; let model = this.form.model; let value = this.fieldValue; let path = this.prop; if (path.indexOf(':') !== -1) { path = path.replace(/:/, '.'); } let prop = getPropByPath(model, path);// if (Array.isArray(value) && value.length > 0) {// this.validateDisabled = true;// prop.o[prop.k] = [];// } else if (value !== this.initialValue) {// this.validateDisabled = true;// prop.o[prop.k] = this.initialValue;// } if (Array.isArray(value)) { this.validateDisabled = true; prop.o[prop.k] = [].concat(this.initialValue); } else { this.validateDisabled = true; prop.o[prop.k] = this.initialValue; } }, //失去焦点触发校验 onFieldBlur() { this.validate('blur'); }, //change触发校验 onFieldChange() { if (this.validateDisabled) { this.validateDisabled = false; return; } this.validate('change'); } }, mounted () { // 收集字段 if (this.prop) { this.dispatch('iForm', 'on-form-item-add', this); Object.defineProperty(this, 'initialValue', { value: this.fieldValue }); this.setRules(); } }, //删除字段 beforeDestroy () { this.dispatch('iForm', 'on-form-item-remove', this); } };</script>
以上为Form整个组件的源码分析,整体来看,Form组件上其实就定义了一些校验规则,通过$on监听FormItem dispath上来的字段再通过用户触发validate去校验整个表单,方法内部并没有直接去获取校验用户输入的数据的逻辑,真正校验数据还是FormItem去做的,所以每个FormItem上会定义一个prop,通过prop进来的key获取对应的规则和用户输入的数据,底层校验还是用的AsyncValidator这个开源库, 至于Input , Checkbox等其他表单穿件主要就是对一些原生表单控件实现v-model和样式控制封装而已。