如何"实现"一个form组件--卡卡西🤣🤣

489 阅读2分钟

本文有以下几个内容

  • 为什么要封装一个form组件
  • 封装的实现原理及参照
  • 如何发布到npm

开发之前需要掌握的知识

  • 一点点 vue组件通讯方式 provide/inject$dispatch/$broadcase (自己实现)方法
  • 一点点 递归的知识
  • 一点点 事件绑定机制
  • 一点点 $on$emit 知识
  • 一点点 $attr$listen 知识 怀疑我在为一点点代言🤣🤣🤣🤣

为什么要封装一个form组件

因为在某个老项目中,用到了mintui这个ui框架,看着小伙伴写的表单校验异常痛苦,到了我这里,觉得不能忍了,就去研究了一下并写了组件。研究对象为 element-ui,对开发element-ui的大佬深深佩服,其中大部分代码是element-ui的源码搬运过来的。如果你们项目用的框架有form组件,本文可以不看呀。

封装的实现原理及参照

我们需要封装 formform-item组件,而且form组件传入modelrules等字段。form-item组件传入 proprequiredlabel等字段,支持 v-model,然后 rules中支持async-validator库的方法。同时我们需要写一些校验不通过的样式。ok,以上就是我们这次需要实现的目标。后面的代码都是mintui框架来编写。

编写mixins,实现组件通讯,自定义dispatchbroadcase方法,

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    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) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

form组件

  • props
props: {
  model: {
    type: Object,
  },
  rules: {
    type: Object,
  },
  validateOnRuleChange: {
    type: Boolean,
    default: true,
  },
},
  • 注入 form自身,方便slot中使用
provide() {
  return {
    MintForm: this,
  };
},
data() {
    return {
      fields: [],
    };
 },

  • 声明 fields变量来收集要校验的field,初始化的时候监听添加删除操作
data() {
  return {
    fields: [],
  };
},
created() {
  this.$on("mint.form.addField", (field) => {
    if (field) {
      this.fields.push(field);
    }
  });
  this.$on("mint.form.removeField", (field) => {
    if (field.prop) {
      this.fields.splice(this.fields.indexOf(field), 1);
    }
  });
},
  • methods中定义validate方法,提交时校验表单
validate(callback) {
  if (!this.model) {
    console.warn(
      "[Element Warn][Form]model is required for validate to work!"
    );
    return;
  }

  let promise;
  // if no callback, return promise
  if (typeof callback !== "function" && window.Promise) {
    promise = new window.Promise((resolve, reject) => {
      callback = function (valid) {
        valid ? resolve(valid) : reject(valid);
      };
    });
  }

  let valid = true;
  let count = 0;
  // 如果需要验证的fields为空,调用验证时立刻返回callback
  if (this.fields.length === 0 && callback) {
    callback(true);
  }
  let invalidFields = {};
  this.fields.forEach((field) => {
    field.validate("", (message, field) => {
      if (message) {
        valid = false;
      }
      invalidFields = objectAssign({}, invalidFields, field);
      if (
        typeof callback === "function" &&
        ++count === this.fields.length
      ) {
        callback(valid, invalidFields);
      }
    });
  });

  if (promise) {
    return promise;
  }
},

form-item 组件

  • html
<div class="mint-form-item" :class="[{'is-error': validateState === 'error'}]">
  <div class="mint-form-item__label" v-if="label">{{label}}</div>
  <div class="mint-form-item-content">
    <slot></slot>
    <transition name="mint-zoom-in-top">
      <slot v-if="validateState === 'error'" name="error" :error="validateMessage">
        <div class="mint-form-item__error">{{validateMessage}}</div>
      </slot>
    </transition>
  </div>
</div>
  • props
props: {
  prop: String,
  error: String,
  required: {
    type: Boolean,
    default: undefined,
  },
  label: String,
},
  • data中声明校验信息validateMessage和校验状态validateState
data() {
  return {
    validateMessage: "",
    validateState: "",
  };
},
  • mixins,混入 dispath 方法,通知 form 组件增加/删除监听
mixins: [emitter],
  • methods 中编写 form-item的校验方法validate,使用async-validator。监听事件触发的类型的方法addValidateEvents,并在mounted中去调用。
methods: {
  validate(trigger, callback = () => {}) {
    this.validateDisabled = false;
    const rules = this.getFilteredRule(trigger);
    if ((!rules || rules.length === 0) && this.required === undefined) {
      callback();
      return true;
    }

    this.validateState = "validating";

    const descriptor = {};
    if (rules && rules.length > 0) {
      rules.forEach((rule) => {
        delete rule.trigger;
      });
    }
    descriptor[this.prop] = rules;

    const validator = new AsyncValidator(descriptor);
    const model = {};

    model[this.prop] = this.fieldValue;
    validator.validate(
      model,
      { firstFields: true },
      (errors, invalidFields) => {
        this.validateState = !errors ? "success" : "error";
        this.validateMessage = errors ? errors[0].message : "";

        callback(this.validateMessage, invalidFields);
        this.elForm &&
          this.elForm.$emit(
            "validate",
            this.prop,
            !errors,
            this.validateMessage || null
          );
      }
    );
  },
  addValidateEvents() {
    const rules = this.getRules();

    if (rules.length || this.required !== undefined) {
      this.$on("mint.form.blur", this.onFieldBlur);
      this.$on("mint.form.change", this.onFieldChange);
    }
  },
  // ...
},
mounted(){
  if (this.prop) {
    this.dispatch("mintForm", "mint.form.addField", [this]);

    let initialValue = this.fieldValue;
    if (Array.isArray(initialValue)) {
      initialValue = [].concat(initialValue);
    }
    Object.defineProperty(this, "initialValue", {
      value: initialValue,
    });

    this.addValidateEvents();
  }
}

ok,大功告成,我们已经初步实现了formform-item,来试一下。

<mt-form ref="form" :model="formModel" :rules="formRules">
  <mt-form-item label="测试" prop="testProp">
    <mt-field v-mode="formModel.name"></mt-field>
  </mt-form-item>
</mt-form>
<mt-button @click="submit">default</mt-button>
submit() {
  this.$refs.form.validate((res) => {
    console.log(res);
    // 这里可以得出是否校验通过
  });
}

已经可以通过提交来判断表单是否校验通过了,但是我们还无法解决blur事件和chage事件,从代码中来看,我们监听了mint.form.blurmint.form.change这两个事件,

// form-item 中监听的
this.$on("mint.form.blur", this.onFieldBlur);
this.$on("mint.form.change", this.onFieldChange);

需要在对应的组件中去触发他们。那我们开始吧

  • blur事件,一般是input,这里我们就对mintuifield进行改造。当你以为只是一个简单的改造时,你发现field的组件,竟然没有注册blur事件!!!这里用了一个原生的写法,手动的给它增加了一个blur事件。
<div>
    <mt-field ref="input" v-bind="$attrs" v-on="$listeners" @change="handChange" />
</div>
export default {
  name: "mint-input",
  mixins: [emitter],
  methods: {
    injectionInputFn() {
      this.$nextTick(() => {
        const inputInstance = this.$refs.input;
        const inputInstanceDom = inputInstance.$refs.input;
        const that = this;
        inputInstanceDom.onblur = function (event) {
          inputInstance.$emit("blur", event);
          that.dispatch("mintFormItem", "mint.form.blur", [this.value]);
        };
      });
    },
    handChange(val) {
      this.dispatch("mintFormItem", "mint.form.change", [val]);
    },
  },
  mounted() {
    this.injectionInputFn();
  },
};
  • change事件,常规操作,目前封装radiochecklist了。
// radio
<template>
  <div>
    <mt-radio v-bind="$attrs" v-on="$listeners" @change="handChange"></mt-radio>
  </div>
</template>


<script>
import emitter from "../../mixins/emitter";
export default {
  name: "mt-form-radio",
  mixins: [emitter],
  methods: {
    handChange(val) {
      this.$emit("change", val);
      this.dispatch("mintFormItem", "mint.form.change", [val]);
    },
  },
};
</script>
// checklist
<template>
  <div>
    <mt-checklist v-bind="$attrs" v-on="$listeners" @change="handChange"></mt-checklist>
  </div>
</template>

<script>
import emitter from "../../mixins/emitter";
export default {
  name: "mt-form-checklist",
  mixins: [emitter],
  methods: {
    handChange(val) {
      this.$emit("change", val);
      this.dispatch("mintFormItem", "mint.form.change", [val]);
    },
  },
};
</script>

ok,搞完之后,我们已经可以正常使用一个formform-item来做业务了。快速迭代,持续修复!

如何发布到npm

我是看了这个文章来实现的 基于vue-cli3创建libs库。按照步骤来即可。唯一需要注意的地方是,打包出来后,css文件和js文件是分开的。可以通过阅读文档 vue构建lib来进行设置不分开。不过一般不推荐这种做法,我们可以在使用的时候进行引入。

import mintuiform from 'mintuiform'
import 'mintuiform/lib/common.css'
Vue.use(mintuiform)

注意事项

  • 一定要注入mintui,然后再使用form
  • form-itemlabelmintui组件的title有些冲突,最好用label
  • mintui的样式有点怪,建议使用方重写样式,覆盖。form组件在组件内覆盖了mintui的样式,不影响全局,只针对于本身组件。 写的丑陋,就不放github地址了😂😂😂😂