• 如何设计好一个组件


    思路

    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%留给用户。

    五、设计用例

    // 用例
    
        
            
        
        
            
        
    
    
    
    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 表单

    1. // App.js
    2. "form" :model="info" :rules="rules">
    3. <i-form-item prop="username">
    4. <i-input v-model="info.username">i-input>
    5. i-form-item>
    6. <i-form-item prop="password">
    7. <i-input v-model="info.password">i-input>
    8. i-form-item>
    9. data() {
    10. return {
    11. info: {
    12. username: '',
    13. password: ''
    14. },
    15. rules: {
    16. username: [
    17. {required: true, message: '用户名不能为空', trigger: 'blur'},
    18. ],
    19. password: [
    20. {required: true, message: '密码不能为空', trigger: 'blur'},
    21. ]
    22. }
    23. }
    24. },
    25. methods: {
    26. handleSubmit() {
    27. this.$refs.form.validate((valid) => {
    28. if (valid) {
    29. window.alert('成功');
    30. } else {
    31. window.alert('表单校验失败');
    32. }
    33. })
    34. }
    35. }
    1. // Form.vue
    2. <script>
    3. export default {
    4. name: 'iForm',
    5. provide() {
    6. return {
    7. form: this
    8. }
    9. },
    10. props: {
    11. model: {
    12. type: Object
    13. },
    14. rules: {
    15. type: Object
    16. }
    17. },
    18. beforeCreate() {
    19. console.log('form-beforeCreate');
    20. },
    21. created() {
    22. this.$on('on-form-item-add', (field) => {
    23. if (field) this.fields.push(field);
    24. })
    25. this.$on('on-form-item-remove', (field) => {
    26. if (field.prop) {
    27. this.fields.splice(this.fields.indexOf(field), 1);
    28. }
    29. })
    30. },
    31. methods: {
    32. resetFields() {
    33. this.fields.forEach(field => {
    34. field.resetFields();
    35. })
    36. },
    37. validate(callback) {
    38. return new Promise(resolve => {
    39. let valid = true;
    40. let count = 0;
    41. this.fields.forEach(field => {
    42. field.validate('', errors => {
    43. if (errors) {
    44. valid = false;
    45. }
    46. if (++count === this.fields.length) {
    47. // 全部完成
    48. resolve(valid);
    49. if (typeof callback === 'function') {
    50. callback(valid);
    51. }
    52. }
    53. })
    54. })
    55. })
    56. }
    57. },
    58. data() {
    59. return {
    60. fields: []
    61. }
    62. },
    63. mounted() {
    64. }
    65. }
    66. script>
    67. <style>
    68. style>
    1. // formItem.vue
    2. <script>
    3. import AsyncValidator from 'async-validator';
    4. import Emitter from '../../mixins/emitter';
    5. export default {
    6. name: 'iFormItem',
    7. mixins: [ Emitter ],
    8. inject: [
    9. 'form'
    10. ],
    11. props: {
    12. label: {
    13. type: String,
    14. default: ''
    15. },
    16. // username 还需要拿到对应的值
    17. prop: {
    18. type: String
    19. }
    20. },
    21. // 组件渲染时,将实例缓存在 Form 中
    22. mounted() {
    23. if (this.prop) {
    24. this.dispatch('iForm', 'on-form-item-add', this);
    25. this.initialValue = this.fieldValue;
    26. this.setRules;
    27. }
    28. },
    29. beforeDestroy() {
    30. this.dispatch('iForm', 'on-form-item-remove', this);
    31. },
    32. data() {
    33. return {
    34. validateState: '',
    35. validateMessage: '',
    36. isRequired: false
    37. }
    38. },
    39. computed: {
    40. // 从 Form 的 model 中动态得到当前表单组件的数据
    41. fieldValue() {
    42. return this.form.model[this.prop];
    43. }
    44. },
    45. methods: {
    46. setRules() {
    47. let rules = this.getRules();
    48. if (rules.length) {
    49. rules.every((rule) => {
    50. // 如果当前校验规则中有必填项,则标记出来
    51. this.isRequired = rule.required;
    52. })
    53. }
    54. this.$on('on-form-blur', this.onFieldBlur);
    55. this.$on('on-form-change', this.onFieldChange);
    56. },
    57. onFieldBlur() {
    58. console.log('onFieldBlur');
    59. // 接收到改变之后进行校验
    60. this.validate('blur');
    61. },
    62. onFieldChange() {
    63. console.log('onFieldChange');
    64. this.validate('change');
    65. },
    66. // 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
    67. getRules() {
    68. let formRules = this.form.rules;
    69. formRules = formRules ? formRules[this.prop] : [];
    70. return [].concat(formRules || []);
    71. },
    72. // 只支持 blur 和 change, 过滤出符合要求的 rule 规则
    73. getFilteredRule (trigger) {
    74. const rules = this.getRules();
    75. return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
    76. },
    77. // 重置数据
    78. resetField() {
    79. this.validateState = '';
    80. this.validateMessage = '';
    81. this.form.model[this.prop] = this.initialValue;
    82. },
    83. validate(trigger, callback = function() {}) {
    84. // 交给数据校验 看看是不是符合
    85. // 查看类型
    86. let rules = this.getFilteredRule(trigger);
    87. if (!rules || rules.length === 0) {
    88. return true;
    89. }
    90. // 设置状态
    91. this.validateState = 'validating';
    92. // 调用别人库来进行校验
    93. let descriptor = {};
    94. descriptor[this.prop] = rules;
    95. const validator = new AsyncValidator(descriptor);
    96. let model = {};
    97. model[this.prop] = this.fieldValue;
    98. console.log(model);
    99. validator.validate(model, { firstFields: true }, errors => {
    100. this.validateState = !errors ? 'success' : 'error';
    101. this.validateMessage = errors ? errors[0].message : '';
    102. callback(this.validateMessage);
    103. })
    104. }
    105. }
    106. }
    107. script>
    108. <style>
    109. .i-form-item-label-required:before {
    110. content: '*';
    111. color: red;
    112. }
    113. .i-form-item-message {
    114. color: red;
    115. }
    116. style>
    1. // input.vue