想到以往写表单,后端的模板里加上一堆东西,还有JS代码用来组织不正确的表单,还要加上HTML5里的新input标签,就算这样还需要在后端验证。虽然后端验证是避免不了的,但是学了Vue之后,发现还有很多库可以用,这个Vuelidate就是很方便的验证库。

Vuelidate的官网是https://vuelidate.netlify.com/,文档也在其中。开始吧

安装和导入

安装没有什么可说的了,npm install vuelidate --save搞定

之后就像普通的插件一样,使用之前需要在项目里导入:

import Vue from 'vue'
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

如果是直接在页面中使用,则直接可以导入:

<script src="vuelidate/dist/vuelidate.min.js"></script>

为字段设置验证功能

还是用学axios的库来学习Vuelidate。

给一个字段加上验证功能主要有如下步骤:

  1. 项目中导入Vuelidate
  2. 在组件中添加新属性和设置验证器
  3. 在绑定属性的标签上添加一个@input事件,通过$v对象获取验证结果
  4. 根据验证结果进行其他逻辑

signup.vue中,是一个用于向Firebase新注册用户的表单,原来是没有任何验证的,现在要给这个表单加上验证功能。首先是email字段。

先到main.js中导入Vuelidate

import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

然后我们到signup.vue组件中,给组件添加一个新的属性validations,这个属性只有在使用Vuelidate的时候才生效。

export default {
    data() {
        return {
            email: '',
            age: null,
            password: '',
            confirmPassword: '',
            country: 'usa',
            hobbyInputs: [],
            terms: false
        }
    },

    validations :{
        email: {
        }
    },

    ......
}

这里的validations中的每一个属性,都表示要验证的字段名称,名称必须和data中定义的属性名称一样,这里的email属性名就对应data中的email属性。

之后就要开始针对这个字段添加验证器了,验证器需要从Vuelidate的包中导入,然后设置在字段对应的属性中,添加如下代码:

import {required, email} from 'vuelidate/lib/validators';
export default {
    data() {
        return {
            email: '',
            age: null,
            password: '',
            confirmPassword: '',
            country: 'usa',
            hobbyInputs: [],
            terms: false
        }
    },

        validations :{
            email:{
                required,
                email
            }
        },

    ......
}

这表明给这个字段添加验证器,验证器需要从Vuelidate的包中导入,之后作为一个属性添加到email对象上,这里使用了ES6的语法,同名绑定,实际上属性名字可以任意取。

做好关联之后,是设置动作,也就是在双向绑定emailinput元素中添加:

<input
    type="email"
    id="email"
    <span style="color: red">@input="$v.email.$touch()"</span>
    v-model="email">
    {{$v}}

监听input事件,v-model也监听这个事件,所以这里实际上在同样一个事件上加上了处理。

$v.email.$touch()中的$v就是验证对象,在后边显示出来看看其中的内容。之后的email就是刚才设置的新属性。而$touch()方法是一个特殊的方法,调用这个方法表示进行验证,此时$v中的内容才会有变化。

正因为如此,$touch()的调用时机不一定是这里,也可以是按下提交按钮之前,或者其他任何逻辑的时候。

现在来启动项目试试,看看添加了验证器之后会有什么变化:

在Email的Input框为空的时候,即刚进入页面,看到如下显示:

{
	"email":{
		"required": false,
		"email": true,
		"$model": "",
		"$invalid": true,
		"$dirty": false,
		"$anyDirty": false,
		"$error": false,
		"$anyError": false,
		"$pending": false,
		"$params": {
			"required": {
				"type": "required"
				},
			"email":
				{ "type": "email" }
		}
	},
	"$model": null,
	"$invalid": true,
	"$dirty": false,
	"$anyDirty": false,
	"$error": false,
	"$anyError": false,
	"$pending": false,
	"$params": { "email": null }
}

这其实是一个验证结果对象,看其中的email的最开始两个属性,就是我们定义的requiredemail验证结果。required要求不能不输入,所以显示false,而email之所以显示true,是因为默认初始状态是true

到框里随便输入几个字符,但不要输入一个完整电子邮件地址,会看到其中的required变成了true,而email变成了false,实际上整个$v对象也有变化:

{
    "email": {
        "required": true,
        "email": false,
        "$model": "cony",
        "$invalid": true,
        "$dirty": true,
        "$anyDirty": true,
        "$error": true,
        "$anyError": true,
        "$pending": false,
        "$params": {
            "required": {
                "type": "required"
            },
            "email": {
                "type": "email"
            }
        }
    },
    "$model": null,
    "$invalid": true,
    "$dirty": true,
    "$anyDirty": true,
    "$error": true,
    "$anyError": true,
    "$pending": false,
    "$params": {
        "email": null
    }
}

email属性中的几个验证结果解释如下:

  1. $model,实际输入的值
  2. $invalid,如果任何一个验证器没通过,就是true
  3. $dirty,表示是不是从未点击过这个控件
  4. $anydirty,任何一个框被点击过
  5. $error,在$invalid$dirty都为true的情况下为true,表示用户点击过但仍未输入正确,这是为了防止进入页面,用户还没碰过这个框的时候就报错。
  6. $anyError,顾名思义,任一个$error出错的时候为true
  7. $pending,表示正在验证当前的这个字段,在异步验证的时候有用。
  8. $params,表示当前字段设置的所有验证器的列表。

其后的属性表示整个表单的验证结果,也就是说如果设置了多个验证字段,都会在当前组件的同一个$v对象中反映出来。

现在输入一个完整的电子邮件地址,再来看看结果:

{
    "email": {
        "required": true,
        "email": true,
        "$model": "conyli@gmail.com",
        "$invalid": false,
        "$dirty": true,
        "$anyDirty": true,
        "$error": false,
        "$anyError": false,
        "$pending": false,
        "$params": {
            "required": {
                "type": "required"
            },
            "email": {
                "type": "email"
            }
        }
    },
    "$model": null,
    "$invalid": false,
    "$dirty": true,
    "$anyDirty": true,
    "$error": false,
    "$anyError": false,
    "$pending": false,
    "$params": {
        "email": null
    }
}

可以看到除了dirty两个验证结果之外,其他都没有任何错误了。

当然,在实际项目中不能够直接打印$v对象,而是通过检查其属性,将错误信息反映在界面上。

将验证结果反映在UI上

可以发现,只要根据$v对象提供的各种属性的布尔值来进行判断,就可以得到当前应该提醒用户的内容,进而将其反映在UI上就可以了。

考虑如下几个场景:

  1. 用户在刚进入界面的时候,requiredfalse,但此时dirtytrue,所以不应该提示用户错误信息
  2. 如果dirtyfalserequired有错,应该进行提示
  3. 如果dirtyfalseemail有错,应该进行提示
  4. $error表示$invalidtruedirty也是true,所以是通用提示,表示用户输入了之后仍未正确

基于上述逻辑,可以编写代码如下:

<div class="input" :class="{invalid: $v.email.$error}">
    <label for="email" :style="{color: $v.email.$error? 'red':''}">Mail</label>
    <input
            type="email"
            id="email"
            @input="$v.email.$touch()"
            v-model="email">
</div>

编写样式如下:

.input.invalid input {
    border: 1px solid red;
    background-color: #ffc9a9;
}

之后如果录入错误,可以看到输入框的颜色和label均改变。

然后可以添加一些文字提示:

<div class="input" :class="{invalid: $v.email.$error}">
    <label for="email" :style="{color: $v.email.$error? 'red':''}">Mail</label>
    <input
            type="email"
            id="email"
            @blur="$v.email.$touch()"
            v-model="email">
    <p v-if="!$v.email.email">请输入正确的Email地址。</p>
    <p v-if="!$v.email.required && $v.email.$dirty">Email地址不能为空。</p>
</div>

红字的部分结合其中的各种验证条件,显示不同的错误信息。这里要注意的就是一开始用户还没有输入的时候,不能去提示错误,这个时候$v对象提供的$dirty就派上用场了。

input框失去焦点之后进行验证

在上边的例子里,只要一开始输入,监听的input方法就会开始判断是否输入错误,但实际上用户在输入的过程中被提示错误,显得不太好,这个时候可以绑定blur,让input框失去焦点的时候才判断。

<div class="input" :class="{invalid: $v.email.$error}">
    <label for="email" :style="{color: $v.email.$error? 'red':''}">Mail</label>
    <input
            type="email"
            id="email"
            @blur="$v.email.$touch()"
            v-model="email">
    <p v-if="!$v.email.email">请输入正确的Email地址。</p>
    <p v-if="!$v.email.required && $v.email.$dirty">Email地址不能为空。</p>
</div>

这样在完成输入,移动到下一个输入框的时候,就会提示错误。

一些其他种类的验证器

现在已经用过了required, email验证器,了解了基础工作原理,可以来看看在其他字段上运用过滤器了。

数字和大小值验证

为age输入框来添加验证器,想验证的是年龄必须大于18岁:

import {required, email, numeric, minValue } from 'vuelidate/lib/validators';

validations :{
    email:{
        required,
        email
    },
    age :{
        required,
        numeric,
        minValue:minValue(18)
    }
}

这里导入了新的验证器,一个数字验证器,一个minValue,是一个函数,需要传入一个最小值。类似的验证器还有验证最大值,验证字符串的大小长度等。

现在再给age框添加上验证事件和UI提示:

<div class="input" :class="{invalid: $v.age.$error}">
    <label for="age">Your Age</label>
    <input
            id="age"
            @blur="$v.age.$touch()"
            v-model="age">
    <p v-if="!$v.age.required && $v.age.$dirty">年龄不能为空。</p>
    <p v-if="!$v.age.numeric && $v.age.$dirty">必须输入数字。</p>
    <p v-if="!$v.age.minValue && $v.age.$dirty &&$v.age.numeric">年龄必须大于{{ $v.age.$params.minValue.min }}岁。</p>
</div>

这里要注意的是,如果把input框的类型限定,比如type=”number”,会导致numeric的属性不会正常变化,所以一般需要验证的可以放开input的类型。

这里新加的内容就是获取这些带参数的验证器其中的参数,以动态的展示限制条件。详细的各个参数如何访问可以查看官方文档。

密码相等

常用的验证之一是验证两个密码是否相等。

这里需要使用一个新的验证器叫做sameAs,这也是一个带参数的验证器,还有两种写法。

分成两步,第一步先给上边的密码输入框添加验证:

import {required, email, numeric, minValue, minLength } from 'vuelidate/lib/validators';

validations :{
    ......
    password: {
        required,
        minLength: minLength(4)
    }
}

UI就简单一些,不做提示只修改样式:

<div class="input" :class="{invalid: $v.password.$error}">
    <label for="password">Password</label>
    <input
            type="password"
            id="password"
            @blur="$v.password.$touch()"
            v-model="password">
</div>

第二步,就是给下边一个密码框添加验证相同密码的验证功能:

import {required, email, numeric, minValue, minLength, sameAs } from 'vuelidate/lib/validators';

confirmPassword: {
    //直接传入字符串
    // sameAs: sameAs("saner")

    //传入当前实例的属性值
    sameAs: sameAs(v=>v.password)
}

同样也设置上UI:

<div class="input" :class="{invalid: $v.confirmPassword.$error}">
    <label for="confirm-password">Confirm Password</label>
    <input
            type="password"
            id="confirm-password"
            @blur="$v.confirmPassword.$touch()"
            v-model="confirmPassword">
</div>

其他的验证都无需设置,因为会自动盯住上一个密码框的变化。

仅在特定情况下才进行验证

紧接着密码下边是一个选择国家的SELECT元素,假如我们有特定的要求,比如如果选中国家是德国,则不需要点击下边的Accept Terms of Use选项。来看看如何实现:

import {required, email, numeric, minValue, minLength, sameAs, requiredUnless} from 'vuelidate/lib/validators';

terms: {
    required: requiredUnless(v=>{
        return v.country === 'germany'
    })
}

这样就行了,很方便的搞定了。不过不知道为什么,在我的机器上checkbox的required不会变化,后来在stackflow上找到了答案:

新版本的Vuelidate中对CHECKBOX的验证修改了,未选中情况下的false值默认也满足required条件;但没有关系,反正绑定了v-model,可以知道按钮选中与否,可以使用下边的代码来验证是否选中:

myCb : { sameAs: sameAs( () => true ) }

想达到和教程一样的效果,需要编写一个自定义验证器,同时也知道了自定义验证器的第二个参数传入的是当前的Vue实例对象:

terms: {
    required: (val,vm)=>{
        if ( vm.country === 'germany') {
            return true;
        }
        return vm.terms === true;
    }
},

验证数组

我们采用v-for的形式渲染了hobbyInputs系列控件,在把hobbyInputs以JSON格式发送之前,如何验证整个数组的有效性。

答案是除了验证数组本身的长度等数据之外,还需要以嵌套的形式验证其内部的每一个数据。编写的验证稍微有些复杂,但很容易懂:

hobbyInputs :{
    required,
    minLength: minLength(1),
    //$each表示当前array中的每一个元素
    //所以其中要对这个元素的属性进行验证
    //$each代表的是hobbyInput对象,要验证其value,验证器中的属性就是value
    $each: {
        value: {
            required,
            minLength:minLength(5)
        }
    }
}

$each代表数组中的每一个元素,在这个例子中,就是hobbyInput对象,等于我们要对hobbyInputvalue属性再编写验证器,就写成了这种嵌套的。

然后需要将验证器添加到表单里:

<div class="hobby-list">
    <div
            class="input"
            v-for="(hobbyInput, index) in hobbyInputs"
            :key="hobbyInput.id">
        <label :for="hobbyInput.id">Hobby #{{ index }}</label>
        <input
                type="text"
                :id="hobbyInput.id"
                @input="$v.hobbyInputs.$each[index].value.$touch()"
                v-model="hobbyInput.value">
        <button @click="onDeleteHobby(hobbyInput.id)" type="button">X</button>
    </div>
    <p v-if="!$v.hobbyInputs.minLength || !$v.hobbyInputs.required">You have to select at least {{ $v.hobbyInputs.$params.minLength.min }} hobby(s).</p>
</div>

注意看绑定的事件,需要给$each加上一个索引,就是数组的索引,在v-for中取得,然后指定验证字段value,最后也加上$touch(),其实和验证普通单个数据一样,多出来的是通过数组和索引选择到具体对象的过程。

在UI错误信息里,也可以通过$params获取传入的参数。

修改提交按钮

一个很常见的情况,是有错误的情况下不允许提交表单。

实际上很简单,这个时候就可以用到$v.$invalid$v$invalid在所有子验证全部的$invalid都是false的情况下返回false,由于$invalid不检测dirty,所以使用这个比使用$error要好。

<button :disabled="$v.$invalid" type="submit">Submit</button>

自定义验证器

既然是验证,肯定都会提供自定义验证器的功能。来看看Vuelidate的验证器如何编写。

所谓一个验证器,其实就是一个自定义的名称,然后对应一个接受一个参数并且返回一个布尔值的函数。

email加自定义验证器:

email: {
    required,
    email,
    myVal: val => {
        return false
    }
},

可以打印出$v或者用Chrome的Vue开发工具查看,$v.email下边多了一个myVal,一直返回false,导致$invalid永远为true,不会通过任何验证。

那么就可以来编写任意的条件了,比如:

email: {
    required,
    email,
    myVal: val => {
        return val !== 'test@test.com'
    }
},

如果邮件名等于这个,就无法通过验证。

自定义验证器的功能远不止如此。

异步验证

在开发中常见的是,用户输入之后,异步查询服务器然后告诉用户是否可用。

自定义验证器还可以返回一个Promise对象,Vuelidate会自动根据返回结果来判断验证通过与否。来模拟一个异步效果:

myVal: val => {
    if (val === '') return true
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(val === 'test@test.com')
        }, 2000)
    })
}

返回了一个两秒钟之后返回val是否等于test@test.com的结果。

启动项目,在地址栏输入test@test.commyVal会过两秒才会变成true

注意观察Chrome的Vue插件,可以发现在这个过程中,前边一直没有用到的$pending变成了true,在返回异步请求之后,$pending又会变回false

这实际上就是异步的验证,在异步验证没有返回结果之前,$pending会变成true

有了模拟的异步之后,就可以在其中使用axios向后端查询实现各种验证,比如验证该用户名是否已经被注册。具体的实现就需要后端配合了。