这段时间在进行一个新项目的前期搭建,新项目框架采用vue-cli3和typescirpt搭建。因为项目比较轻量,所以基本没有使用额外的ui组件,有时候我们需要的一些基础组件我就直接自己开发了。今天就来介绍一下如何利用vue的自定义指令directive来开发一个表单验证插件的过程。

1.vue插件开发

关于vue的插件开发,官方文档里有很清晰的说明,详情可以去阅读开发文档。我自己开发的表单验证插件validate.ts和loading,messageBox插件都是利用了这种方式。今天先来看表单验证插件的开发。

  • vue全局指令
// myPlugin.js
export default {
  install: (Vue, options) => {
    // 注册一个my-directive指令
    Vue.directive('my-directive', {
      bind(el, binding, vnode, oldVnode) {
        // 逻辑
      }
      ...
    })
  }
}
// main.js
import Vue from 'vue';
import myPlugin from 'myPlugin';
Vue.use(myPlugin);

复制代码

上面是注册一个vue指令插件的写法。值得注意的是注册自定义指令的时候,bind()函数为指令的钩子函数,其中的参数el表示指令绑定的元素,可以直接操作DOM。binding表示一个对象,包括指令名称,绑定值等信息。vnode和oldVnode表示Vue编译生成的虚拟节点。

我们通过注册一个全局指令v-validateParams指令,绑定到输入表单的input标签上来校验当前输入值是否符合要求。

2.v-validateParams指令

最开始我参考了网上的一些代码。基础的实现如下:

  • 整体框架
import Vue from 'vue'
export default {
  install: (Vue, options) => {
    // 注册一个全局自定义指令 `v-validateParams`
    Vue.directive('validateParams', {
      // 当被绑定的元素插入到 DOM 中时
      inserted: function (el, binding, vNode) {
        // 给指令绑定的Dom元素添加事件监听,监测输入框失焦事件
        // 每次当表单中的输入框失焦时执行函数
        el.addEventListener('blur', function (event) {
          // 1.首先重置所有错误提示
          // 2.获取自定义指令中传入的校验规则参数和表单输入的值
          // 3.依次判断当前输入的值是否符合校验规则
        })
      }
    })

    // 注册一个全局自定义指令 `v-validateSubmit`,这个指令绑定到表单的提交button上
    Vue.directive('validateSubmit', {
      // 当被绑定的元素插入到 DOM 中时
      inserted: function (el, binding, vNode) {
        // 给提交button添加事件监听
        el.addEventListener('click', function (event) {
          // 获取当前组件内所有含有v-check类名的元素
          let elements = vNode.context.$el.getElementsByClassName('v-check')
          var evObj = vNode.context.$el.createEvent('Event')
          evObj.initEvent('blur', true, true)
          for (let element of elements) {
            // 给所有v-check元素绑定blur事件
            element.dispatchEvent(evObj);
          }
          // 获取当前组件下的所有错误提示元素
          let errorInputs = vNode.context.$el.getElementsByClassName('input-error');
          // 如果组件中没有错误提示元素,则执行当前组件实例中的submit()函数
          if(errorInputs.length === 0){
            vNode.context.submit();
          }
        })
      }
    })
  }
}

复制代码

这里需要着重说明一下validateSubmit指令,这个指令绑定到提交按钮上,在点击的时候执行校验,校验通过之后执行提交操作。但是这里的实现方式不是特别友好:

1.需要获取当前组件中的所有input元素,给他们绑定并执行blur事件,以此来执行validateParams指令中的校验逻辑。

2.需要获取当前组件中的所有错误提示元素,如果他们存在就不能执行提交操作。

3.当组件内不含任何错误提示元素时,就表示校验通过,执行当前组件内的submit函数,所以每个表单组件的提交函数都只能命名为submit

然后我们再看下指令validateParams,该指令需要绑定到表单input元素上,并把校验规则当作参数写入。当该input元素失焦时,会执行指令中给当前元素绑定的事件中的逻辑。这些逻辑分为三个步骤,我已经写在注释里了,现在我们来看下具体实现。

  • 重置所有错误提示
/**
 * 重置当前节点样式
 * @param el: HTMLElement,传入当前绑定的input元素
 */
const resetError = (el: HTMLElement) => {
  el.className = el.className.replace('input-error', '').trim();
  if ( el.parentNode ) {
    const ErrorNode = el.parentNode.querySelector('.error-tips');
    if (ErrorNode) {
      el.parentNode.removeChild(ErrorNode);
    }
  }
};
复制代码
  • 获取自定义指令中传入的校验规则参数和表单输入的值
// binding.value是传入自定义指令的参数,以数组的形式
for (const rule of binding.value) {
  // 分别获取到自己定义的校验规则并执行
  const { min, max, message, required, pattern } = rule;
  if ( min && InputEl.value.length < min ) {
    // 如果不符合校验,执行报错函数
    validateError(InputEl, message);
    break;
  }
  if ( max && InputEl.value.length > max ) {
    validateError(InputEl, message);
    break;
  }
  if ( !!required && !InputEl.value ) {
    validateError(InputEl, message);
    break;
  }
  if ( pattern && !pattern.test(InputEl.value) ) {
    validateError(InputEl, message);
    break;
  }
  if ( rule && typeof rule === 'function' ) {
    rule(vNode.context, InputEl.value, validateError, InputEl);
    break;
  }
}
复制代码
  • 校验不符合,执行报错函数
/**
 * 执行错误提示函数,用input-error 类名和含有错误信息的p元素表示未通过校验
 * @param el: HTMLElement,传入当前绑定的input元素
 * @param errorMsg: string,传入错误提示信息
 */
const validateError = (el: HTMLElement, errorMsg: string) => {
  if (Array.prototype.includes.call(el.classList, 'input-error')) {
    //如果当前组件里已经有了错误提示信息,什么也不做
    return;
  } else {
    const errorNode = document.createElement('p');
    errorNode.className = 'error-tips';
    errorNode.textContent = errorMsg;
    if (el.parentNode) {
      // 在当前input 元素后追加一个p元素,内容为错误提示
      el.parentNode.appendChild(errorNode);
    }
    // 在当前input 元素上添加一个input-error类名
    el.className += ' input-error';
  }
};
复制代码

现在我就把自己实现的这个表单校验插件大致说完了,下面我们看下具体使用。

3.自定义校验指令v-validateParams使用

首先新建校验规则文件:

// rules.ts
export const required = (message) => ({
  message,
  required: true
});
export const min = (message, length=3) => ({
  message,
  min: length
})
export const max = (message, length=15) => ({
  message,
  max: length
})
export const pattern = (message, reg) => ({
  message,
  pattern: reg
})

// form.vue
<template>
  <div>
    <div class="form-item">
      <label for="userEmail">用户名:</label>
      <input id="userEmail" class='v-check' type="text" v-model="userName"
        v-validateParams="[inputNameRequired, inputNameMin, inputNameMax, inputNamePattern]">
    </div>
    <button class="btn" v-if="show" type="success" v-checkSubmit>确认</button>
  </div>
</template>
<script lang='ts'>
import { Component, Vue, Prop } from 'vue-property-decorator';
import { max, min, required, name, pattern} from 'rules';

@Component({
  components: {},
})
export default class Auth extends Vue {
  private show: boolean = true;
  private userName: string = '';
  private inputNameMax = max('请不要超过20个字符');
  private inputNameMin = min('请不要小于3个字符');
  private inputNameRequired = required('请输入用户名');
  private inputNamePattern  = pattern('请输入符合要求的用户名', /^[a-zA-Z0-9_-]{4,16}$/);
  private submit() {
    alert('通过校验');
  }
}
</script>
复制代码

通过这个例子我们可以看到,使用时需要将校验规则引入并赋给vue实例中的数据。然后在模板中,需要给input标签添加v-check类名,再使用v-validateParams指令,并传入参数。提交按钮需要调用v-checkSubmit指令。按照这种方式就能够使用自己开发的这个表单校验插件。

4. 当前方式存在的问题

虽然表单校验可以使用了,但是存在一些显而易见的问题:

1.js和html耦合度较高,插件还需要获取dom元素,组件的html模板中还需要添加指定的类名。

2.在vue中使用dom操作,不符合vue的设计思路,实现方式也不优雅。

3.校验规则的校验逻辑在指令定义时写定了,添加或删除都需要改动插件代码。

4.提交指令根据当前组件内的是否含有特定dom来判断当前校验状态,且执行提交的函数名称也在指令逻辑中写定了。

我根据现有一个demo结合着自己的需求来实现的这个表单校验插件,开发的过程中我已经知道这么写问题很多,甚至不能称之为一个合格的插件。同时也清楚的认识到自己的javascript水平还很初级,需要很大进步。

当前开发的表单插件的主要问题在于如何将插件中的校验状态返回到组件内。我们可以在插件内维护一个事件处理函数,将校验规则传入并校验,再将校验结果直接传给组件内。这样就可以避免大量的dom操作。之后我需要尽快对这个插件进行更科学合理的重构。

参考文章

vue插件

vue自定义指令

vue使用自定义指令实现表单校验

重构:从 0.1 构建一个 Vue 表单验证插件

va.js——Vue 表单验证插件的写作过程


作者简介: 宫晨光,人和未来大数据前端工程师。