# 简介

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

# 1. vue插件开发

关于vue的插件开发,官方文档里有很清晰的说明,详情可以去阅读开发文档 (opens new window)。这次开发的表单验证插件validate.ts就是利用了这种方式。

  • 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自定义指令 (opens new window)的写法,注册的时候,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 ( !!required && !InputEl.value ) {
    // 如果不符合校验,执行报错函数
    validateError(InputEl, message);
    break;
  }
  if ( min && InputEl.value.length < min ) {
    validateError(InputEl, message);
    break;
  }
  if ( max && InputEl.value.length > max ) {
    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. 表单插件的使用

首先新建校验规则文件:

// 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" 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({})
export default class Auth extends Vue {
  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操作。之后我需要尽快对这个插件进行更科学合理的重构。

# 更新:对表单插件的优化 (opens new window)

# 参考文章

vue插件 (opens new window)

vue自定义指令 (opens new window)

vue使用自定义指令实现表单校验 (opens new window)

重构:从 0.1 构建一个 Vue 表单验证插件 (opens new window)

va.js——Vue 表单验证插件的写作过程 (opens new window)


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