前端開發中,表單的校驗一個很常見的功能,一些 ui 庫例如 ant.design 與 Element ui 都實現了有校驗功能的 Form 元件。async-validator 是一個可以對資料進行非同步校驗的庫,ant.design 與 Element ui 的 Form 元件都使用了 async-validator。本文就簡單介紹一下 async-validator 的基本用法以及使用該庫實現一個簡單的有校驗功能的 Form 元件。
1. async-validator 的基本用法
async-validator 的功能是校驗資料是否合法,並且根據校驗規則給出提示資訊。
下面演示一下 async-validator 的最基本用法。
import AsyncValidator from 'async-validator'
// 校驗規則
const descriptor = {
username: [
{
required: true,
message: '請填寫使用者名稱'
},
{
min: 3,
max: 10,
message: '使用者名稱長度為3-10'
}
]
}
// 根據校驗規則構造一個 validator
const validator = new AsyncValidator(descriptor)
const data = {
username: 'username'
}
validator.validate(model, (errors, fields) => {
console.log(errors)
})
複製程式碼
當資料不符合校驗規則時,在 validator.validate 的回撥函式中,就可以得到相應的錯誤資訊。
當 async-validator 中常見的校驗規則無法滿足需求時,我們可以編寫自定義的校驗函式來校驗資料。一個簡單的校驗函式如下。
function validateData (rule, value, callback) {
let err
if (value === 'xxxx') {
err = '不符合規範'
}
callback(err)
}
const descriptor = {
complex: [
{
validator: validateData
}
]
}
const validator = new AsyncValidator(descriptor)
複製程式碼
async-validator 支援對資料非同步校驗,所以在編寫自定義校驗函式時,不管校驗是否通過,校驗函式中的 callback 都要呼叫。
2. 編寫 Form 元件與 FormItem 元件
現在知道了 async-validator 的使用方法,如何將這個庫跟要編寫的 Form 元件結合起來呢。
實現思路
用一張圖描述一下實現思路。
Form 元件
Form 元件應該是一個容器,裡面包含不定數量的 FormItem 或者其他元素。可以使用 Vue 內建的 slot 元件來代表 Form 裡面的內容。
Form 元件還需要知道包含了多少個需要校驗的 FormItem 元件。一般情況下,父子元件的通訊 是通過在子元件上繫結事件實現的,但是這裡使用 slot,無法監聽到子元件的事件。這裡可以在 Form 元件上通過 $on 監聽事件,FormItem 掛載或者銷燬前觸發 Form 元件的自定義事件即可。
按照這個思路,我們先編寫 Form 元件。
<template>
<form class="v-form">
<slot></slot>
</form>
</template>
<script>
import AsyncValidator from 'async-validator'
export default {
name: 'v-form',
componentName: 'VForm', // 通過 $options.componentName 來找 form 元件
data () {
return {
fields: [], // field: {prop, el},儲存 FormItem 的資訊。
formError: {}
}
},
computed: {
formRules () {
const descriptor = {}
this.fields.forEach(({prop}) => {
if (!Array.isArray(this.rules[prop])) {
console.warn(`prop 為 ${prop} 的 FormItem 校驗規則不存在或者其值不是陣列`)
descriptor[prop] = [{ required: true }]
return
}
descriptor[prop] = this.rules[prop]
})
return descriptor
},
formValues () {
return this.fields.reduce((data, {prop}) => {
data[prop] = this.model[prop]
return data
}, {})
}
},
methods: {
validate (callback) {
const validator = new AsyncValidator(this.formRules)
validator.validate(this.formValues, (errors) => {
let formError = {}
if (errors && errors.length) {
errors.forEach(({message, field}) => {
formError[field] = message
})
} else {
formError = {}
}
this.formError = formError
// 讓錯誤資訊的順序與表單元件的順序相同
const errInfo = []
this.fields.forEach(({prop, el}, index) => {
if (formError[prop]) {
errInfo.push(formError[prop])
}
})
callback(errInfo)
})
}
},
props: {
model: Object,
rules: Object
},
created () {
this.$on('form.addField', (field) => {
if (field) {
this.fields = [...this.fields, field]
}
})
this.$on('form.removeField', (field) => {
if (field) {
this.fields = this.fields.filter(({prop}) => prop !== field.prop)
}
})
}
}
</script>
複製程式碼
FormItem 元件
FormItem 元件就簡單很多,首先要向上找到包含它的 Form 元件。接下來就可以根據 formError 計算出對應的錯誤資訊。
<template>
<div class="form-item">
<label :for="prop" class="form-item-label" v-if="label">
{{ label }}
</label>
<div class="form-item-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'form-item',
computed: {
form () {
let parent = this.$parent
while (parent.$options.componentName !== 'VForm') {
parent = parent.$parent
}
return parent
},
fieldError () {
if (!this.prop) {
return ''
}
const formError = this.form.formError
return formError[this.prop] || ''
}
},
props: {
prop: String,
label: String
}
}
</script>
複製程式碼
FormItem 在 mounted 與 beforeDestroy 鉤子中還需要觸發 Form 元件的一些自定義事件。
<script>
export default {
// ...
methods: {
dispatchEvent (eventName, params) {
if (typeof this.form !== 'object' && !this.form.$emit) {
console.error('FormItem必須在Form元件內')
return
}
this.form.$emit(eventName, params)
}
},
mounted () {
if (this.prop) {
this.dispatchEvent('form.addField', {
prop: this.prop,
el: this.$el
})
}
},
beforeDestroy () {
if (this.prop) {
this.dispatchEvent('form.removeField', {
prop: this.prop
})
}
}
}
</script>
複製程式碼
最後新建一個 index.js
匯出編寫好的元件。
import VForm from './Form.vue'
import FormItem from './FormItem.vue'
export {
VForm,
FormItem
}
複製程式碼
3. 使用方式
表單的校驗函式是在 Form 元件中。通過 $ref 可以訪問到 Form 元件,呼叫 validate 函式,從而獲取到相應的校驗資訊。
使用方法如下:
<template>
<v-form :model="formData" :rules="rules" ref="form">
<form-item label="手機號" prop="tel">
<input type="tel" maxlength="11" v-model.trim="formData.tel"/>
</form-item>
<button @click="handleSubmit">儲存</button>
</v-form>
</template>
<script>
import { VForm, FormItem } from './common/Form'
export default {
data () {
return {
formData: {
tel: ''
},
rules: {
tel: [
{required: true, message: '您的手機號碼未輸入'},
{pattern: /^1[34578]\d{9}$/, message: '您的手機號碼輸入錯誤'}
]
}
}
},
methods: {
handleSubmit () {
this.$refs.form.validate(errs => {
console.log(errs)
})
}
},
components: {
VForm,
FormItem
}
}
</script>
複製程式碼
完整的程式碼點選這裡。
4. 總結
本文簡單介紹了一下 async-validator 的用法,並實現了一個有校驗功能的 Form 元件。這裡的實現的 Form 表單存在著很多的不足:(1) 僅僅是在提交表單時才去校驗。(2) FormItem 元件也應該根據校驗的結果調整 ui,給出相應的提示。所以,Form 元件更適合在互動較少的移動端使用。
大家可以根據這個實現思路,根據應用場景,編寫定製化更高的 Form 元件。