如何设计好一个组件

思路

  1. 分析主要功能
  2. 分析所有功能点
  3. 拆分组件(单一原则)
  4. 设计组件API接口(props、event、slot)
  5. 将各个功能散发到各个组件(解耦)
  6. 构建关系(组件通信)

具体(通过Form表单来验证)

一、分析主要功能

用以收集、校验、提交数据。

二、分析所有功能点

  • 单个表单数据的校验
  • 所有表单数据的校验
  • 清空所有表单数据
  • 清空单个表单数据

三、拆分组件(单一原则)

  • Form 作为表单控件,用来管理整体数据的收集提交。按照单一原则来领取 Form 组件功能。(所有表单数据的校验、清空所有表单数据)。
  • 输入框、选择器、单选框、多选框等控件用来绑定数据,当数据改变之后需要去校验数据。按照单一原则不应该由表单内部来认领校验功能,如果校验给到表单,那么其他地方只是想用表单控件不需要校验数据。
  • FormItem 作为表单控件的包裹层,用来分担校验功能。认领单个表单数据的校验、清空单个表单数据。

四、设计组件API

Form:作为数据收集和数据校验数据

一、props

  • 数据:model
  • 校验:rules

二、event:

  • 不需要,是由用户主动调动,而不是触发事件返回给用户。

三、slot

  • 作为 FormItem 的分发地

FormItem:作为校验单项表单

一、props

  • 需要校验值:prop

二、event:

  • 不需要,由 Form 组件主动调起。

三、slot

  • 需要,作为最后一层,一方面是承载表单控件。
  • 另一方面完成80%的功能,剩下20%留给用户。

五、设计用例

// 用例
<i-form :model="info" :rules="rules">
    <i-form-item prop="username">
        <i-input v-model="info.username"></i-input>
    </i-form-item>
    <i-form-item prop="password">
        <i-input v-model="info.password"></i-input>
    </i-form-item>
</i-form>


data() {
    return {
        info: {
            username: '',
            password: ''
        },
        rules: {
            username: [
                {required: true, message: '用户名不能为空', trigger: 'blur'},
            ],
            password: [
                {required: true, message: '密码不能为空', trigger: 'blur'},
            ]
        }
    }
}

五、将各个功能散发到各个组件(解耦)

Form

  • 整体校验:validates() {}
  • 整体重置:resetFields() {}

FormItem

  • 单个表单校验:validate() {}
  • 单个表单重置:resetField() {}

六、构建关系(组件通信)

一、问题:

  1. 问题:FormItem 保存单前表单的校验,那么 Form 如何能够主动通知 FormItem 去校验当前表单数据。
  2. 解决:通过在组件渲染过程中,Form 去收集每一个 FormItem 的实例。

  1. 问题:FormItem 在校验数据过程中,如何获取到当前表单传入的数据和规则。
  2. 解决:通过依赖注入,FormItem 去获取 Form 的实例,来获取数据和规则。

  1. 问题:FormItem 如何知道自己什么时候进行校验数据。
  2. 解决:表单控件(Input)在数据改变之后去通知 FormItem 去进行校验一次数据。

实践

带着上面的思路来分析下面的 form 表单

// App.js
<i-form ref="form" :model="info" :rules="rules">
    <i-form-item prop="username">
        <i-input v-model="info.username"></i-input>
    </i-form-item>
    <i-form-item prop="password">
        <i-input v-model="info.password"></i-input>
    </i-form-item>
</i-form>


data() {
    return {
        info: {
            username: '',
            password: ''
        },
        rules: {
            username: [
                {required: true, message: '用户名不能为空', trigger: 'blur'},
            ],
            password: [
                {required: true, message: '密码不能为空', trigger: 'blur'},
            ]
        }
    }
},
methods: {
  handleSubmit() {
      this.$refs.form.validate((valid) => {
          if (valid) {
              window.alert('成功');
          } else {
              window.alert('表单校验失败');
          }
      })
  }
}
// Form.vue
<template>
  <form>
      <slot></slot>
  </form>
</template>

<script>
export default {
    name: 'iForm',
    provide() {
        return {
            form: this
        }
    },
    props: {
        model: {
            type: Object
        },
        rules: {
            type: Object
        }
    },
    beforeCreate() {
        console.log('form-beforeCreate');
    },
    created() {
        this.$on('on-form-item-add', (field) => {
            if (field) this.fields.push(field);
        })
        this.$on('on-form-item-remove', (field) => {
            if (field.prop) {
                this.fields.splice(this.fields.indexOf(field), 1);
            }
        })
    },
    methods: {
        resetFields() {
            this.fields.forEach(field => {
                field.resetFields();
            })
        },
        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) {
                            // 全部完成
                            resolve(valid);
                            if (typeof callback === 'function') {
                                callback(valid);
                            }
                        }
                    })
                })
            })
        }
    },
    data() {
        return {
            fields: []
        }
    },
    mounted() {
    }
}
</script>

<style>

</style>
// formItem.vue
<template>
  <div>
      <label v-if="label" :class="{ 'i-form-item-label-required': isRequired }">{{label}}</label>
      <div>
          <slot></slot>
           <div v-if="validateState === 'error'" class="i-form-item-message">{{ validateMessage }}</div>
      </div>
  </div>
</template>

<script>
import AsyncValidator from 'async-validator';
import Emitter from '../../mixins/emitter';
export default {
    name: 'iFormItem',
    mixins: [ Emitter ],
    inject: [
        'form'
    ],
    props: {
        label: {
            type: String,
            default: ''
        },
        // username 还需要拿到对应的值
        prop: {
            type: String
        }
    },
    // 组件渲染时,将实例缓存在 Form 中
    mounted() {
        if (this.prop) {
            this.dispatch('iForm', 'on-form-item-add', this);

            this.initialValue = this.fieldValue;

            this.setRules;
        }
    },
    beforeDestroy() {
        this.dispatch('iForm', 'on-form-item-remove', this);
    },
    data() {
        return {
            validateState: '',
            validateMessage: '',
            isRequired: false
        }
    },
    computed: {
        // 从 Form 的 model 中动态得到当前表单组件的数据
        fieldValue() {
            return this.form.model[this.prop];
        }
    },
    methods: {
        setRules() {
            let rules = this.getRules();
            if (rules.length) {
                rules.every((rule) => {
                    // 如果当前校验规则中有必填项,则标记出来
                    this.isRequired = rule.required;
                })
            }
            this.$on('on-form-blur', this.onFieldBlur);
            this.$on('on-form-change', this.onFieldChange);
        },
        onFieldBlur() {
            console.log('onFieldBlur');
            // 接收到改变之后进行校验
            this.validate('blur');
        },
        onFieldChange() {
            console.log('onFieldChange');
            this.validate('change');
        },
        // 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
        getRules() {
            let formRules = this.form.rules;
            formRules = formRules ? formRules[this.prop] : [];
            return [].concat(formRules || []);
        },
        // 只支持 blur 和 change, 过滤出符合要求的 rule 规则
        getFilteredRule (trigger) {
            const rules = this.getRules();
            return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
        },
        // 重置数据
        resetField() {
            this.validateState = '';
            this.validateMessage = '';
            this.form.model[this.prop] = this.initialValue;
        },
        validate(trigger, callback = function() {}) {
            // 交给数据校验  看看是不是符合
            // 查看类型
            let rules = this.getFilteredRule(trigger);

            if (!rules || rules.length === 0) {
                return true;
            }
            
            // 设置状态
            this.validateState = 'validating';

            // 调用别人库来进行校验
            let descriptor = {};
            descriptor[this.prop] = rules;

            const validator = new AsyncValidator(descriptor);
            let model = {};
            model[this.prop] = this.fieldValue;
            console.log(model);
            validator.validate(model, { firstFields: true }, errors => {
                this.validateState = !errors ? 'success' : 'error';
                this.validateMessage = errors ? errors[0].message : '';
                callback(this.validateMessage);
            })
        }
    }
}
</script>
<style>
  .i-form-item-label-required:before {
    content: '*';
    color: red;
  }
  .i-form-item-message {
    color: red;
  }
</style>
// input.vue
<template>
  <input 
        type="text"
        :value="currentValue"
        @input="handleInput"
        @blur="handleBlur">
</template>

<script>
import Emitter from '../../mixins/emitter';
export default {
    name: 'iInput',
    mixins: [ Emitter ],
    props: {
        value: {
            Type: String,
            default: ''
        }
    },
    data() {
        return {
            currentValue: this.value
        }
    },
    watch: {
        value(newVal) {
            this.currentValue = newVal;
        }
    },
    methods: {
        handleInput(event) {
            const value = event.target.value;
            this.currentValue = value;
            this.$emit('input', value);
            this.dispatch('iFormItem', 'on-form-change', value);
        },
        handleBlur() {
            this.dispatch('iFormItem', 'on-form-blur', this.currentValue);
        }
    }
}
</script>

<style>

</style>
// emitter.js
function broadcast(componentName, eventName, params) {
    this.$children.forEach(child => {
      const name = child.$options.name;
  
      if (name === componentName) {
        child.$emit.apply(child, [eventName].concat(params));
      } else {
        broadcast.apply(child, [componentName, eventName].concat([params]));
      }
    });
  }
  export default {
    methods: {
      dispatch(componentName, eventName, params) {
        let parent = this.$parent || this.$root;
        let name = parent.$options.name;
  
        while (parent && (!name || name !== componentName)) {
          parent = parent.$parent;
  
          if (parent) {
            name = parent.$options.name;
          }
        }
        if (parent) {
          parent.$emit.apply(parent, [eventName].concat(params));
        }
      },
      broadcast(componentName, eventName, params) {
        broadcast.call(this, componentName, eventName, params);
      }
    }
};

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值