vue进阶知识-02-手写一个element的form表单组件

560 阅读4分钟

前言:通⽤表单组件(form表单)是各大项目中几乎是非常常用的了,那么我们怎么不依赖ui框架自己去实现一个呢?实现收集数据、校验数据并提交。本文详细记录了实现的每一个步骤及优化。

需要的技术支持

  • dispatch(componentName)
  • provide/inject
  • async-validator
  • $attrs
  • mixins

用法及分析

element中form使用

我们自定义用法示例:

<SwForm :model='userInfo' :rules="rules" ref='swform'>
  <SwFormItem label="用户名:" prop='acc'>
    <SwFormInput
      v-model="userInfo.acc"
      placeholder="请输入用户名"
    ></SwFormInput>
  </SwFormItem>
  <SwFormItem label="密码:" prop='pwd'>
    <SwFormInput
      type="password"
      v-model="userInfo.pwd"
      placeholder="请输入用户名"
    ></SwFormInput>
  </SwFormItem>
  <SwFormItem>
      <button @click="login">登录</button>
  </SwFormItem>
</SwForm>
 data() {
    return {
      userInfo: {
        acc: "",
        pwd: "",
      },
      rules: {
        acc: [{ required: true, message: "用户名不得低于5位", min: 5 }],
        pwd: [
          {
            required: true,
            message: "密码不低于5位不超过10位",
            min: 5,
            max: 10,
          },
        ],
      },
    };
  },

需求分析

  • 首先有输入框input,只是用来输入信息的,有type、placeholder等属性,在使用的地方v-model双向绑定。
  • 然后有formitem,有label、prop属性,还应该有校验、错误提示信息
  • 然后还有form,用来做全局校验

SwFormInput

新建components/form文件夹,再新建SwFormInput.vueindex.vue文件。

index.vue文件:

<template>
  <div>
    <SwFormInput
      v-model="userInfo.acc"
      placeholder="请输入用户名"
    ></SwFormInput>
    <!-- 测试双向绑定 -->
    <p>{{ userInfo.acc }}</p>
  </div>
</template>

<script>
import SwFormInput from "./SwFormInput";
export default {
  components: { SwFormInput },
  data() {
    return {
      userInfo: {
        acc: "",
      },
      rules: {
        acc: [
          {
            required: true,
            message: "用户名不低于5位不超过10位",
            min: 5,
            max: 10,
          },
        ],
      },
    };
  },
};
</script>

SwFormInput.vue文件:

<template>
  <div>
    <template>
    <!-- 因为可能不同的input传递的属性是不一样的,所以使用$attrs来展开获得 -->
      <input type="text" @input="input" v-bind="$attrs" />
    </template>
  </div>
</template>

<script>
export default {
  methods: {
    input() {
      // 实现双向绑定需要实现: 1.:value  2. 监听input事件
      this.$emit("input", event.target.value);
    },
  },
};
</script>

这个时候我们发现,SwFormInput文件中div也加上了placeholder属性,这个叫属性的继承。在script中加上一个属性:inheritAttrs设置为false,关闭属性继承,避免设置到根元素上。

SwFormItem

新建SwFormItem.vue文件,SwFormItem文件中接收有label、prop属性,还应该有校验、错误提示信息。

<template>
  <div>
    <!-- label标签 -->
    <label v-if="label">{{ label }}</label>
    <!-- 插槽:SwFormInput的位置 -->
    <slot></slot>
    <!-- 验证不通过时候的信息 -->
    <p v-if="error">{{ error }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      error: "", // 验证失败的提示信息,应该是这个组件自己的状态,所以就让组件自己来维护
    };
  },
  props: {
    label: {
      type: String,
      default: "",
    },
    prop: {
      type: String,
      default: "",
    },
  },
};
</script>

在index.vue中引入SwFormItem并使用它。

<SwFormItem label='用户名' prop='acc'>
  <SwFormInput
    v-model="userInfo.acc"
    placeholder="请输入用户名"
  ></SwFormInput>
</SwFormItem>

import SwFormItem from "./SwFormItem";
components: { SwFormInput, SwFormItem },

这个时候,我们能去做验证吗?思考一下,输入框的值,和验证规则,现在在SwFormItem这个组件中能拿取到吗?貌似好像不行是吧。有没有想着,那简单,在index.vue中,把值和规则全传递过来不就完了么,但是第一我们书写的语法就不一样了,第二是每个SwFormItem中都拿取全部的值和全部的规则吗?顺再思考一下,我们传递了prop属性,用来干嘛呢?带着这些疑惑,我们再来做SwForm。

SwForm

新建SwForm.vue文件,SwForm中接收model、rules属性。

<template>
  <div>
    <!-- 插槽: 显示标签内所有的内容 -->
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    model: {
      // 使用SwForm时候传递的所有数据
      type: Object,
      require: true,
    },
    rules: {
      // 使用SwForm时候传递的所有验证规则
      type: Object,
    },
  },
};
</script>

同理在index.vue中引入并使用。

<SwForm :model="userInfo" :rules="rules">
  <SwFormItem label="用户名" prop="acc">
    <SwFormInput
      v-model="userInfo.acc"
      placeholder="请输入用户名"
    ></SwFormInput>
  </SwFormItem>
</SwForm>

import SwForm from "./SwForm";
components: { SwFormInput, SwFormItem, SwForm },

这个时候,我们还做什么操作呢?我们将组件实例作为提供者,⼦代组件可⽅便获取数据和校验规则。provide/inject能够实现祖先和后代之间传值,当我们不使用vuex时,vue提供给了我们这种原生接口的方式来实现隔代传值。

  // script中添加provide
  provide() { // 提供的意思
  // 隔代传参,用法类似于data
    return {
      form: this,  // 把组件实例本身提供给子孙组件
    };
  },

SwForm做为提供者,把自身提供给了子孙组件,我们就在SwFormItem中来注入获取它。

SwFormItem.vue中新增:

inject: ['form'], // 注入,注入需要的属性

验证

SwFormInput中,监听change事件

  <!-- 每次值更改了并且失去焦点就通知验证 那么如何去通知呢? 使用$parent会导致耦合度很高,适应性不强-->
<input type="text" @input="input" v-bind="$attrs" @change="change" />

element官方做法地址

// 广播: 从上到下派发事件
function broadcast(componentName, eventName, params) {
	// componentName: 组件的componentName名
	// eventName:事件名
	// params: 参数,需要是一个数组
	
	// 遍历所有的子组件:树形的向下遍历,只要名字相同,就都派发事件
  this.$children.forEach(child => {
    var name = child.$options.componentName;
	// 如果子组件的componentName和传入的componentName名字相同,就派发事件
	// 需要注意: 组件需要写componentName(和我们在组件中写的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) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
		
	  // 向上查找,直到找到componentName和传入的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);
    }
  }
};

src下新建mixins混入文件,新增emitter.js,把上面的代码复制进去。

SwFormInput中:

<script>
import emitter from "@/mixins/emitter.js";
export default {
  mixins: [emitter],
  inheritAttrs: false,
  methods: {
    input() {
      // 实现双向绑定需要实现: 1.:value  2. 监听input事件
      this.$emit("input", event.target.value);
    },
    change() {
      // this.dispatch:  参数1: 组件的componentName  参数2: 触发的方法
      // 在需要触发的组件中 写上componentName,注意不是name  ->  在SwFormItem组件中写上componentName:'SwFormItem',
      this.dispatch('SwFormItem', 'validate');
    }
  },
};
</script>

SwFormItem的script中添加:

mounted() {
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      console.log("validate");
    },
  },

测试是否成功。没问题之后,我们开始验证。

在validate方法中,我们需要做验证,第一:怎么拿取值和规则;第二:有了规则和值怎么验证。

先来看第一步,怎么拿取值和对应的规则。我们通过provide/inject传递并接收了form实例,在这儿就可以使用拿取值了。

validate() {
    const rule = this.form.rules[this.prop];
    const value = this.form.model[this.prop];
},

拿取到值和规则之后,我们就可以进行下一步:验证了。

使用插件async-validator

安装:

npm i async-validator -D

使用:

import Schema from "async-validator";
  mounted() {
     // 监听验证的通知进行验证
    this.$on("validate", () => {
      this.validate().catch(() => {
        // 为了防止promise错误不被捕获而报错
        console.log();
      });
    });
  },
  methods: {
    validate() {
      // 获取规则并执⾏校验
      // 拿取值
      const value = this.form.model[this.prop];
      // 拿取校验规则
      const rule = this.form.rules[this.prop];
      // 创建校验器
      // Schema的参数:key是校验字段,value是校验规则
      const validator = new Schema({ [this.prop]: rule });
      // 执行校验, 返回Promise,没有触发catch就说明验证通过
      return new Promise((resolve, reject) => {
        // validate参数: key是校验字段, value是校验值
        validator.validate({ [this.prop]: value }, (err) => {
          if (err) {
            // 如果有错误,校验失败
            this.error = err[0].message;
            reject(err[0].message); // 抛出错误
          } else {
            this.error = "";
            resolve();
          }
        });
      });
    },
  },

全局验证

index.vue中添加一个button:

<SwForm :model="userInfo" :rules="rules" ref="swform">
  <SwFormItem label="用户名" prop="acc">
    <SwFormInput
      v-model="userInfo.acc"
      placeholder="请输入用户名"
    ></SwFormInput>
  </SwFormItem>
  <SwFormItem>
    <button @click="login">登录</button>
  </SwFormItem>
</SwForm>
  methods: {
    login() {
      // 找到SwForm实例,调用它的validate方法,会得到结果 ->  成功或者失败
      this.$refs.swform.validate((result) => {
        if (result) {
          alert("suc");
        } else {
          alert("err");
        }
      });
    },
  },

SwForm.vue中:

  methods: {
    validate(callback) {
      // 遍历所有儿子中含有prop属性的,然后执行他们的validate方法 ->  方法会返回promise ->  放到一个数组validates中
      const validates = this.$children
        .filter((item) => item.prop)
        .map((item) => item.validate());
      Promise.all(validates) // 使用all方法全部执行,全部成功才算校验通过,有一个失败就失败
        .then(() => callback(true))
        .catch(() => callback(false));
    },
  },

到此,我们就完成了element的form表单的自定义实现。

优化

在上一步SwForm.vue中:validate方法里面通过this.$children获取并遍历含有prop属性的,然后执行他们的validate方法,要递归获取所有的子组件,这个是会影响性能的。官方的做法是: 每个需要校验的SwFormItem实例,向SwForm传递自身,然后在SwForm中就可以拿取到所有的需要校验的SwFormItem组件实例了。

做法:SwFormItem.vue中:

import emitter from '@/mixins/emitter';

 mixins: [emitter],
 
 mounted() {
    // 监听验证的通知进行验证
    this.$on("validate", () => {
      this.validate().catch(() => {
        // 为了防止promise错误不被捕获而报错
        console.log();
      });
    });

    if(this.prop) { // 如果有prop属性 那么就代表需要进行验证
    // 派发事件通知SwForm,在SwForm中新增自己 ->  把自己传递过去
      this.dispatch('SwForm', 'formItenField', [this])
    }
  },

SwForm中:

 componentName: 'SwForm',
 data() {
    return {
      field: [], // 用来存放所有需要校验的SwFormItem组件
    };
  },
  
  created() {
    this.$on("formItenField", (item) => {
      this.field.push(item);
    });
  },
  
  validate(callback) {
      const validates = this.field.map((item) => item.validate());
      Promise.all(validates)
        .then(() => callback(true))
        .catch(() => callback(false));
    },

样式

样式就自己微调了

代码地址