iview Form组件源码分析

iview Form组件源码分析

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和样式控制封装而已。