不论是PC
端仍是移动端,提交表单是一个常见场景,那么友好交互的表单校验也是一个常见场景了。vue
最近本人在开发移动端页面,也碰见了表单需求,倒霉的是技术选型中的UI
库不符合业务场景,因而~~~~node
我走上了一条本身动手,而后但愿丰衣足食的道路.....正则表达式
不过当我将思路想清楚以后,我发现实现过程比我想象得简单了不少!如今我来记录一下个人思路。数组
因为咱们的产品经理没提表单交互要求(果真随便才是最难的),因而我就成了上帝,想怎么折腾就怎么折腾 ^ _ ^bash
因而我就根据经验和业务场景,设计了一下需求:app
根据业务场景,在实现表单校验的过程当中,我封装了三个组件:Form
、FormItem
、Input
,它的结构应该下面这样的:ide
下面是我对他们的功能分配:函数
Form
Form
组件重点不在UI
,它的重点在于逻辑实现,好比实现校验(局部)表单,重置表单等这些功能。工具
之因此将这些功能放在Form
组件中,是由于想和UI
表单一致,将整个表单包装成一个总体。ui
而若是将这些功能放在FormItem
组件中,就是将表单打散了。并且,若是校验整个表单或重置表单时,咱们就须要操做一个一个FormItem
,这样会让咱们的代码很难读。
FormItem
表单项组件重点在UI
,主要呈现表单项标签和校验信息
其实这里"输入框"指的是能和用户交互的部分,好比select
、input
、radio
、textArea
等。根据业务场景,我是对input
进行了封装。
先把场景摆出来~~~
上面的场景就包含两个校验需求:
如今开始一步一步实现表单校验。
如今个人目录结构是这样的:
开始时,我是这样写的:
<template>
<div>
<li-form ref="form">
<li-form-item>
<li-input></li-input>
</li-form-item>
<!-- 省略部分 -->
</li-form>
</div>
</template>
export default {
name: 'app',
data() {
return {
form: {
name: '', // 姓名
phone: '', // 手机号
email: '', // 邮箱,
code: '' // 验证码
}
}
},
components: {
[Form.name]: Form,
[FormItem.name]: FormItem,
[Input.name]: Input
}
}
</script>
复制代码
咱们使用form
来存储用户输入,可是如今的表单只是一个基本的结构,还不具有校验功能。
那么,实现表单校验的第一步是啥???
不知道你想到的第一步是什么,我想到的第一步是实现校验工具
step 1
:实现校验工具校验工具是用来实现校验功能的
对于实现校验工具,我须要思考两个问题:
校验工具一般是一个函数,那么这个函数就有会输入和输出。
校验工具是为了校验用户输入,那么它的输入(参数)就应该是用户的输入和校验规则,因为能够对用户的输入进行多个限制的校验,那么校验规则就应该是一个数组。
校验工具的输出应该是校验结果,校验结果能够是下面几种方式:
Promise.reject() / Promise.resolve()
: 用Promise
方法来表示校验是否经过。Boolean
值我选择的是第二种方法。第一种方法的缺点是,在多个表单组合成一个大表单的时候,若是第一个表单校验不经过(Promise.reject()
)会阻塞后面表单的校验,不符合业务场景。
对于用户的输入,咱们应该从什么角度规定用户的输入是合适的,好比是不是必填项,限制长度、最小长度、最大长度、类型(数字或字符串)等。有时咱们还须要根据具体需求来动态校验。因此,我从下面几个角度来定义校验规则:
假设,咱们如今对用户的姓名作这样的限制:必填,输入长度在1 - 10
;若是用户没有输入,则提示"请输入姓名";若是用户输入的字符超出了10
个字符,则提示"请输入1 - 10
个字符"
// 表单:
const form = {
name: '', // 姓名
phone: '', // 手机号
email: '' // 邮箱
}
// 表单规则:
const rules = {
name: [
{ required: true, message: '请输入姓名' },
{ min: 1, max: 10, message: '请输入1 - 10个字符'}
]
}
复制代码
下面的方法是根据用户的输入,而后返回校验信息,注意:这里不是返回校验结果。
// validator.js
/* rules = [ { required: true, message: '请输入姓名' }, { min: 1, max: 10, message: '请输入1 - 10个字符'} ] */
export const validator = (value, rules) => {
const result = [] // 保存校验信息
rules.forEach(rule => {
let msg = ''
const {
len = 0, // 字段长度
max = 0, // 最大长度
min = 0, // 最小长度
message = '', // 校验文案
pattern = '', // 正则表达式校验
type = 'string', // 类型
required = false, // 是否必填
validator // 自定义函数
} = rule
if (required && !value) {
msg = message || '请输入'
}
// typeValidator: 校验类型
if (type === 'string' && typeValidator(value) && value) {
const length = String(value).length || 0
// lengthValidator: 校验长度
msg = lengthValidator(length, min, max, len, message)
}
if (pattern) {
const isReg = typeValidator(pattern, 'RegExp')
if (!isReg) {
msg = '正则校验规则不正确'
}
if (!pattern.test(value)) {
msg = message || '请输入正确的值'
}
}
if (validator && typeof validator === 'function') {
msg = validator(value, rules)
}
if (msg) {
result.push(msg)
}
})
return result
}
// typeValidator: 类型校验函数,比较粗糙~~~
const baseTypes = ['string', 'number', 'boolean']
const typeValidator = (value, type = 'string') => {
if (baseTypes.includes(type)) {
const valueType = typeof value
return baseTypes.includes(valueType)
} else if (type === 'array') {
return Array.isArray(value)
} else if (type === 'email') {
const reg = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
return reg.test(value)
} else if (type === 'RegExp') {
return value instanceof RegExp
}
}
// lengthValidator: 长度校验函数
const lengthValidator = (length, min, max, len, message) => {
if (len && len !== length) {
return message || `请输入${len}个字符`
}
if (min && length < min) {
return message || `至少输入${min}个字符`
}
if (max && length > max) {
return message || `最多输入${max}个字符`
}
if (min && max && (length < min || length > max)) {
return message || `请输入${min} ~ ${max}个字符`
}
return ''
}
复制代码
若是用户没有输入时,上面的方式执行后,会返回数组:
validator(form.name, rules.name)
// ['请输入姓名', '请输入 1 - 10 个字符']
复制代码
Step 2
: 邮递员送信在上一步中,咱们调用validator
方法就能够获得校验信息,而前面咱们就有说,表单的校验是在Form
组件里实现的,因此,咱们在Form
组件中调用validator
就能够了,而调用validator
须要拿到form
和rules
这两个值,咱们能够经过prop
的形式传递给Form
组件。
因此,开始,Form
组件是这样的:
<!-- form.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { validator } from './validator'
export default {
name: 'li-form',
props: {
data: {
type: Object,
default: () => ({})
},
rules: {
type: Object,
default: () => ({})
}
},
methods: {
// 校验整个表单
validateFields () {
// ...
},
}
}
</script>
复制代码
Form
组件里validateFields
方法来校验整个表单,在这里咱们就要根据校验信息来返回校验结果啦^ _ ^
实现校验整个表单须要思考下面两点:
FormItem
显示校验信息第一个问题咱们已经实现了,只需每一个字段调用validator
就能够了:
// form.vue
validateFields () {
let hasError = false
const ruleKeys = Object.keys(this.rules)
ruleKeys.forEach(ruleKey => {
const value = this.data[ruleKey]
const keyResult = this.validateField(value, ruleKey)
if (!hasError) {
hasError = keyResult.length > 0
}
})
return hasError
}
validateField (value, prop) {
const rules = this.rules[prop]
let keyResult = validator(value, rules)
return keyResult
}
复制代码
上面代码的逻辑以下图所示:
咱们经过遍历每一个字段(key
),将表单(form
)和校验规则(rules
)联系起来,这样就能够获取到每一个字段的值(value
)和对应的校验规则(rule
),最后调用校验工具函数就能够了
第二个问题FormItem
显示校验信息,就好像邮递员送信同样,须要将特定的信送到特定的收信人手上,这里Form
组件就是邮递员,因此如今咱们就须要将信与收信人联系起来,将检验信息给FormItem
,并显示出来
那么咱们怎么将信与收信人联系起来呢?
回忆第一个问题,咱们是经过遍历key
获得了对应的校验信息,那咱们一样能够将key
和FormItem
绑定起来,用key
做为FormItem
的惟一标识:
<template>
<div>
<li-form :data="form" :rules="rules" ref="form">
<li-form-item prop="name">
<li-input></li-input>
</li-form-item>
</li-form>
</div>
</template>
export default {
name: 'app',
data() {
return {
form: {
name: '', // 姓名
},
rules: {
name: [
{ required: true, message: '请输入姓名' },
{ min: 1, max: 10, message: '请输入1 - 10个字符'}
]
}
}
}
</script>
复制代码
如图所示,这样咱们就将FormItem
和检验信息绑定起来:
在这里,将key
和FormItem
绑定起来,具体是使用ref
属性。给FormItem
添加ref
属性后,咱们就能够拿到FormItem
实例,而且能够操做FormItem
的属性和方法。
因此,当咱们获得校验信息后,就能够经过ref
来操做FormItem
实例,让它显示校验信息。
下面是对FormItem
的封装:
<!-- form-item.vue -->
<template>
<div :ref="prop">
<div>
<slot name="label">
<span>{{ label }}</span>
</slot>
</div>
<div>
<slot></slot>
<span>{{ msg }}</span>
</div>
</div>
</template>
<script>
export default {
name: 'li-form-item',
props: {
prop: {
type: String,
default: ''
},
},
data () {
return {
error: [] // 校验信息:['请输入姓名', '请输入 1 - 10 个字符']
}
},
computed: {
msg () {
return this.error[0] // 显示第一个
}
}
}
</script>
复制代码
FormItem
校验信息具体是什么结构,这个就看我的决定了
如今咱们再回过头来改写validateFields
// form.vue
validateFields () {
// ...
const ruleKeys = Object.keys(this.rules)
ruleKeys.forEach(ruleKey => {
const value = this.data[ruleKey]
const keyResult = this.validateField(value, ruleKey)
// ...
}
// ...
},
validateField (value, prop) {
const [vNode] = this.$children.filter(vnode => prop in vnode.$refs)
const rules = this.rules[prop]
let keyResult = []
if (vNode && rules) {
keyResult = validator(value, rules)
vNode.error = keyResult
}
return keyResult
}
复制代码
在页面点击提交的时候,咱们只写下面的代码就能够实现整个表单的校验啦!
this.$refs.form.validateFields()
复制代码
到这里咱们就实现了校验整个表单
并且细心的同窗可能就会发现其实validateField
方法就是校验某个字段的方法。
validateField
方法经过key
来获得FormItem
实例,而且获得来校验信息,可是它返回的是校验信息,是一个数组。在这里,我为了和validateFields
返回结构保持一致,因此我还另外写了一个方法用来校验某个字段
// form.vue
// 校验表单字段
// @params value 表单字段值
// @params label 表单字段名称
validateFieldValue (value, lable) {
let hasError = false
const keyResult = this.validateField(value, lable)
hasError = keyResult.length > 0
return hasError
}
复制代码
这里 validateFieldValue 只能校验一个字段,它完整的功能应该是能够校验多个字段,可是我没有这个业务场景,因此就没写,后面再完善这里。
因此在页面里点击获取短信验证码按钮时,写上:
this.$refs.form.validateFieldValue(this.form.phone, 'phone')
复制代码
这就能够校验单个字段了。
重置表单实现两个功能:
将表单值清空就须要将props
传过来的form
清空,这就是和父组件通讯
移除校验结果和校验整个表单相似,只是不须要执行校验工具函数(validator
)而已
因此,重置表单:
// form.vue
// 重置表单
resetFields () {
let obj = {}
Object.keys(this.data).forEach(key => {
obj = {
...obj,
[key]: null
}
})
this.validateFields(true)
this.$emit('change', obj)
},
// 校验整个表单
validateFields (reset = false) {
// ...
const ruleKeys = Object.keys(this.rules)
ruleKeys.forEach(ruleKey => {
const value = this.data[ruleKey]
const keyResult = this.validateField(value, ruleKey, reset)
}
// ...
},
validateField (value, prop, reset = false) {
// ...
let keyResult = []
if (vNode && rules) {
if (!reset) {
keyResult = validator(value, rules)
}
vNode.error = keyResult
}
return keyResult
},
复制代码
Step 3
: 寄信表单校验功能基本完成了,可是还有一个校验功能没实现,就是在输入框值变化或者失焦的时候出发校验,因此先让咱们来改写一下校验规则:
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 1, max: 10, message: '请输入1 - 10个字符', , trigger: 'change' }
]
}
复制代码
其中trigger
就表示校验是在失焦仍是值变化时触发校验。
那怎么实现呢~~
这里有两种思路:
Input
组件内部再写一个校验方法Form
组件的方法第一种思路实现的核心仍是key
:
key
,首先获得该字段的校验规则Input
组件里调用validator
方法获得校验信息FormItem
显示校验信息思路以下所示
可是这个方法也有问题:
Form
的总体校验和Input
的失焦校验。Input
做为一个输入UI
,不必具有逻辑功能,这使得Input
和父组件、FormItem
组件的耦合性过高了结果一想,这是让Input
和Form
通讯啊,若是采用事件,那必须得在父组件里监听Input
的blur
或者change
事件,这样灵活性过低了。
因此最后,我采用的是第二个方法——触发Form
组件的方法
实现第二种方法,咱们须要解决两个问题:
Input
能拿到Form
组件的实例validateFieldValue (value, lable)
方法,因此咱们还须要知道字段key
这个过程就好像咱们把信写好了,须要去邮局去寄信同样。
因此如今的问题是咱们怎么知道邮局在哪呢?
在又双叒叕看了Vue
的API
后,忽然灵光一现,发现Provide / inject
能够解决个人问题
这对选项须要一块儿使用,以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
Provide
能够向子孙后代注入依赖,那咱们将Form
实例注入Input
就能够啦,另外字段key
值咱们也可使用Provide
^ _ ^
如今咱们先在Form
组件里使用Provide
来向子孙后代注入实例。
// form.vue
export default {
provide () {
return {
liForm: this
}
}
}
复制代码
而后在FormItem
组件里使用Provide
向子孙注入key
:
// form-item.vue
export default {
props: {
prop: {
type: String,
default: ''
}
}
provide () {
return {
formItem: this.prop
}
}
}
复制代码
最后咱们在Input
里使用inject
来接收依赖:
<!-- input.vue -->
<template>
<div>
<input
:value="value"
@blur="$blur"
@change="$change"
@input="$input"
>
</div>
</template>
<script>
export default {
name: 'li-input',
model: {
prop: 'value',
event: 'change'
},
inject: {
liForm: {
default: ''
},
formItem: {
default: ''
}
},
props: {
value: [String, Number]
},
methods: {
$blur (e) {
this.$emit('blur', e)
const value = e.target.value
this.triggerValidate(value, 'blur')
},
$change (e) {
const value = e.target.value
this.$emit('change', value)
},
$input (e) {
const value = e.target.value
this.$emit('input', value)
this.$emit('change', value)
this.triggerValidate(value, 'change')
},
triggerValidate (value, triggerTime) {
if (!(this.liForm && this.formItem)) return
const trigger = this.liForm.getTriggerTimer(this.formItem, triggerTime)
if (trigger === triggerTime) {
this.liForm.validateField(value, this.formItem)
}
}
}
}
</script>
复制代码
Note:在
inject
里给liForm
和formItem
添加一个默认值(default
),不然会报错
liForm.getTriggerTimer
是为了获取字段校验规则里面定义的校验时机:
// form.vue
getTriggerTimer (lable, triggerTime) {
const rules = this.rules[lable]
const ruleItem = rules.find(item => item.trigger === triggerTime)
const { trigger = '' } = ruleItem || {}
return trigger
}
复制代码
到这里,表单校验基本完成啦^ _ ^
开始是打算附上源码,并写个Demo
,但是目前没时间啊,后面会加上。并且这代码写的比较粗糙,也须要完善,不过我写这文章是为了记录个人思路,而后但愿让看到这篇文章的你引发思考,让咱们能有一个思想上的碰撞。
下面来总结一下:
Form
组件实现校验功能,FormItem
显示校验信息,Input
用于用户输入。key
值将Form
、FormItem
和Input
组件联系在一块儿。经过key
值,Form
操做FormItem
实例,让FormItem
显示校验信息。Provide/inject
和key
值,让Input
操做Form
组件,实现失焦或值变化校验。