Vue3 复杂表单校验实战:从基础到 Element UI 高级应用async-validator
前言
在 Vue3 项目中,表单校验是前端开发的核心需求之一。无论是简单的注册表单,还是复杂的表格、树形结构表单,校验逻辑的合理设计直接影响用户体验和系统稳定性。本文将结合 async-validator 基础库与 Element UI 组件库,从基础校验到高级场景(表格+表单、树形控件+表单),全面解析复杂表单校验的实现方案。
一、快速上手:安装与环境配置
1.1 安装依赖
npm install async-validator element-plus --save
# 或
yarn add async-validator element-plus
1.2 核心概念
- async-validator:基于 Promise 的异步校验库,支持自定义规则、异步验证、嵌套对象/数组校验。
- Element UI 表单组件:提供
el-form、el-form-item等组件,内置校验功能,支持与async-validator集成。 - 校验规则(Schema):描述字段的校验要求(如必填、格式、自定义函数等)。
- 校验器实例:通过
new AsyncValidator(rules)创建,负责执行具体校验逻辑。
二、基础场景:简单表单校验(async-validator)
2.1 需求描述
用户注册表单,包含以下字段:
- 用户名(必填,长度 3-12 字符)
- 密码(必填,包含字母+数字,长度 6-16 字符)
- 确认密码(必填,需与密码一致)
2.2 代码实现
2.2.1 定义校验规则(Schema)
// src/utils/validateRules.js
export const registerRules = {
username: [
{ required: true, message: '用户名不能为空' },
{ min: 3, max: 12, message: '用户名长度需在 3-12 字符之间' }
],
password: [
{ required: true, message: '密码不能为空' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,16}$/,
message: '密码需包含字母和数字,长度 6-16 字符'
}
],
confirmPassword: [
{ required: true, message: '确认密码不能为空' }
]
};
2.2.2 Vue3 组件中集成校验
<!-- src/views/Register.vue -->
<template>
<div class="register-form">
<h2>用户注册</h2>
<el-form :model="formData" :rules="registerRules" ref="formRef">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" type="password" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="formData.confirmPassword" type="password" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">注册</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { AsyncValidator } from 'async-validator';
import { registerRules } from '@/utils/validateRules';
const formData = reactive({
username: '',
password: '',
confirmPassword: ''
});
const formRef = ref(null);
const handleSubmit = async () => {
try {
// 执行校验(基于 async-validator)
await formRef.value.validate();
console.log('校验通过,提交数据:', formData);
alert('注册成功!');
} catch (err) {
console.error('校验失败:', err);
}
};
</script>
2.3 关键逻辑说明
- 校验器绑定:通过
el-form的:rules属性绑定校验规则,ref获取校验器实例。 - 触发校验:调用
formRef.value.validate()方法,返回 Promise 并行执行所有校验规则。 - 错误反馈:校验失败时,Element UI 会自动高亮错误字段并显示提示信息。
三、进阶场景:自定义校验与异步校验(async-validator)
3.1 需求升级
增加以下复杂校验逻辑:
- 密码强度校验(自定义函数:至少包含 1 个大写字母、1 个小写字母、1 个数字)。
- 用户名唯一性校验(异步请求:调用后端接口检查用户名是否已存在)。
3.2 自定义校验函数
通过 validator函数实现自定义逻辑,支持同步返回错误信息或抛出异常。
// 更新 registerRules.js
export const registerRules = {
// ...其他规则
password: [
{ required: true, message: '密码不能为空' },
{
validator: (rule, value, callback) => {
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{6,16}$/;
if (!reg.test(value)) {
callback(new Error('密码需包含大小写字母和数字,长度 6-16 字符'));
} else {
callback(); // 校验通过
}
}
}
]
};
3.3 异步校验(用户名唯一性检查)
通过返回 Promise或使用 async/await实现异步校验,适用于需要调用接口的场景。
// 更新 registerRules.js
export const registerRules = {
// ...其他规则
username: [
{ required: true, message: '用户名不能为空' },
{
asyncValidator: async (rule, value) => {
// 模拟异步请求(实际项目中替换为真实 API)
const isUsernameExist = await checkUsernameExist(value);
if (isUsernameExist) {
throw new Error('该用户名已被注册');
}
}
}
]
};
// 模拟接口:检查用户名是否存在
const checkUsernameExist = (username) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(username === 'admin'); // 假设用户名为 "admin" 时已存在
}, 500);
});
};
3.4 组件中适配异步校验
<script setup>
// ...其他代码
const handleSubmit = async () => {
try {
// 异步校验会自动等待 Promise 解决
await formRef.value.validate();
console.log('校验通过');
} catch (err) {
// err.errors 包含异步校验的错误信息
console.error('校验失败:', err.errors);
}
};
</script>
四、高阶场景:复杂对象与数组校验(async-validator)
4.1 需求描述
表单包含 联系地址(省市区三级联动)和 标签列表(动态添加/删除),要求:
- 省份、城市、区县均为必填。
- 标签列表必填(至少 1 个),最多 5 个,且不重复。
4.2 嵌套对象与数组校验规则
// src/utils/validateRules.js
export const complexFormRules = {
address: {
type: 'object', // 声明为对象类型
required: true,
message: '地址信息不能为空',
fields: {
province: { required: true, message: '请选择省份' },
city: { required: true, message: '请选择城市' },
district: { required: true, message: '请选择区县' }
}
},
tags: {
type: 'array', // 声明为数组类型
required: true,
message: '至少选择一个标签',
min: 1,
max: 5,
validator: (rule, value) => {
const uniqueTags = new Set(value);
if (uniqueTags.size !== value.length) {
throw new Error('标签不能重复');
}
}
}
};
4.3 组件中集成嵌套校验
<!-- src/views/ComplexForm.vue -->
<template>
<div class="complex-form">
<h2>复杂表单</h2>
<el-form :model="formData" :rules="complexFormRules" ref="formRef">
<!-- 地址信息 -->
<el-form-item label="省份" prop="address.province">
<el-select v-model="formData.address.province" clearable>
<el-option label="北京市" value="beijing" />
<el-option label="上海市" value="shanghai" />
</el-select>
</el-form-item>
<el-form-item label="城市" prop="address.city">
<el-select v-model="formData.address.city" clearable>
<el-option v-if="formData.address.province === 'beijing'" label="北京市" value="beijing" />
<el-option v-if="formData.address.province === 'shanghai'" label="上海市" value="shanghai" />
</el-select>
</el-form-item>
<el-form-item label="区县" prop="address.district">
<el-select v-model="formData.address.district" clearable>
<el-option v-if="formData.address.city === 'beijing'" label="朝阳区" value="chaoyang" />
<el-option v-if="formData.address.city === 'shanghai'" label="浦东新区" value="pudong" />
</el-select>
</el-form-item>
<!-- 标签列表 -->
<el-form-item label="标签列表">
<el-input
v-model="tagInput"
placeholder="输入标签后按回车"
@keyup.enter="addTag"
/>
<el-tag
v-for="(tag, index) in formData.tags"
:key="index"
closable
@close="removeTag(index)"
>
{{ tag }}
</el-tag>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { AsyncValidator } from 'async-validator';
import { complexFormRules } from '@/utils/validateRules';
const formData = reactive({
address: {
province: '',
city: '',
district: ''
},
tags: []
});
const formRef = ref(null);
const tagInput = ref('');
const handleSubmit = async () => {
try {
await formRef.value.validate();
console.log('校验通过,提交数据:', formData);
alert('提交成功!');
} catch (err) {
console.error('校验失败:', err.errors);
}
};
const addTag = () => {
const newTag = tagInput.value.trim();
if (newTag && !formData.tags.includes(newTag)) {
formData.tags.push(newTag);
tagInput.value = '';
}
};
const removeTag = (index) => {
formData.tags.splice(index, 1);
};
</script>
4.4 关键逻辑说明
-
type: 'object':声明字段为对象类型,通过fields配置子字段的校验规则。 -
type: 'array':声明字段为数组类型,通过min/max控制长度,validator实现自定义逻辑(如去重)。 - 错误定位:嵌套对象错误的
field属性使用点语法(如address.province),数组错误的field属性直接为数组名(如tags)。
五、Element UI 表单高级用法:表格与表单结合校验
5.1 场景描述
在 Element UI 的 el-table中嵌入 el-form表单(如输入框、下拉框),实现表格行数据的动态校验,并在界面上直观显示错误信息。
5.2 实现代码与思路
5.2.1 页面效果与需求
表格中每一行包含“审核状态”的下拉框,需校验每一行的“审核状态”是否选择,校验结果显示在界面上(如红色提示)。
5.2.2 关键代码解析
<template>
<el-card :body-style="{ padding: '100px' }">
<el-form ref="formRef" :model="{ checkList: tableData }" :rules="rules">
<el-table :data="tableData" style="width: 60%" border>
<!-- 其他列 -->
<el-table-column label="审核状态">
<template slot-scope="scope">
<!-- 校验项的 prop 需指定为数组路径:`checkList.${scope.$index}.status` -->
<el-form-item
label="审核状态"
:prop="`checkList.${scope.$index}.status`"
:rules="rules.status"
>
<el-select v-model="scope.row.status" clearable>
<el-option label="通过" value="pass" />
<el-option label="拒绝" value="reject" />
</el-select>
</el-form-item>
</template>
</el-table-column>
</el-table>
<el-button @click="validateForm()" class="mt16">验证</el-button>
</el-form>
</el-card>
</template>
<script>
export default {
data() {
return {
tableData: [
{ date: '2016-05-02', name: '王小虎', status: '' },
{ date: '2016-05-03', name: '王小虎', status: '' }
],
rules: {
status: [
{ required: true, message: '请选择审核状态', trigger: 'change' }
]
}
};
},
methods: {
validateForm() {
this.$refs.formRef.validate((valid) => {
if (valid) {
alert('验证通过');
} else {
alert('验证失败');
}
});
}
}
};
</script>
5.2.3 核心要点
- 动态
prop绑定:表格行的校验项prop需通过checkList.${scope.$index}.status格式指定,其中scope.$index是当前行的索引。 - 校验规则继承:
el-form的:rules中定义的status规则会应用到每一行的校验项。 - 错误反馈:校验失败时,Element UI 会自动在对应行的下拉框旁显示错误提示。
六、Element UI 表单高级用法:树形控件与表单结合校验
6.1 场景描述
在 Element UI 的 el-tree树形控件中嵌入 el-form表单(如单选框),实现树形结构数据的动态校验,并准确定位错误节点。
6.2 实现代码与思路
6.2.1 页面效果与需求
树形控件中每个节点包含“作用域”的单选框,需校验每个节点的“作用域”是否选择,校验结果显示在对应节点旁。
6.2.2 关键代码解析
<template>
<el-card shadow="always">
<el-form :model="{ treeOptions }" :rules="addEditFormRules" ref="ruleForm">
<el-tree :data="treeOptions" node-key="menuId" :props="treeProps">
<span slot-scope="{ data }" class="custom-tree-node">
<span>{{ data.menuName }}</span>
<el-form-item
:prop="getTreeProp(data.menuName)"
:rules="addEditFormRules.scope"
>
<el-radio-group v-model="data.scope">
<el-radio :label="item.value" v-for="item in optionsData" :key="item.value" />
</el-radio-group>
</el-form-item>
</span>
</el-tree>
<el-button @click="validateForm()" type="primary">验证</el-button>
</el-form>
</el-card>
</template>
<script>
export default {
data() {
return {
treeOptions: [/* 树形数据 */],
optionsData: [/* 单选框选项 */],
addEditFormRules: {
scope: [{ required: true, message: '请选择!', trigger: 'change' }]
},
treeProps: { label: 'menuName', children: 'children' }
};
},
methods: {
// 递归查找树形节点路径
findPathToTarget(tree, targetValue, path = []) {
for (let i = 0; i < tree.length; i++) {
const currentNode = tree[i];
path.push(i);
if (currentNode.menuName === targetValue) return path;
if (currentNode.children) {
const result = this.findPathToTarget(currentNode.children, targetValue, path);
if (result) return result;
}
path.pop();
}
return null;
},
// 生成校验 prop(如 `treeOptions.0.children.1.children.0.scope`)
getTreeProp(menuName) {
const path = this.findPathToTarget(this.treeOptions, menuName);
return path ? `treeOptions.${path.join('.children.')}` : '';
},
validateForm() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
alert('验证通过');
} else {
alert('验证失败');
}
});
}
}
};
</script>
6.2.3 核心要点
- 递归路径查找:通过
findPathToTarget函数递归遍历树形数据,生成节点的唯一路径(如treeOptions.0.children.1.children.0)。 - 动态
prop生成:根据节点路径生成校验prop(如treeOptions.0.children.1.children.0.scope),确保校验规则精准定位到目标节点。 - 错误定位:校验失败时,Element UI 会在对应树形节点的单选框旁显示错误提示。
七、常见使用场景总结
| 场景类型 | 典型需求 | 解决方案 |
|---|---|---|
| 基础字段校验 | 必填项、长度限制、格式匹配 | 使用 async-validator的 required、min、max、pattern规则 |
| 自定义逻辑校验 | 密码强度、金额范围 | 通过 validator函数实现自定义校验逻辑 |
| 异步数据校验 | 用户名/手机号唯一性检查 | 使用 asyncValidator返回 Promise 或 async 函数 |
| 嵌套对象校验 | 地址信息(省市区)、用户详情(联系方式+紧急联系人) | 定义 type: 'object'并配置 fields子规则 |
| 数组校验 | 标签列表、SKU 列表 | 使用 type: 'array'+ min/max+ 自定义 validator |
| 表格+表单结合校验 | 表格行内输入框/下拉框校验 | 动态绑定 prop为 数组名.${行索引}.字段名 |
| 树形控件+表单结合校验 | 树形节点内单选框/复选框校验 | 递归查找节点路径,动态生成 prop路径(如 treeOptions.0.children.1.scope) |
八、总结
本文从基础到高级,全面解析了 Vue3 中复杂表单校验的实现方案:
- 基础校验:通过
async-validator快速实现简单字段的必填、格式、长度校验。 - 自定义与异步校验:利用
validator和asyncValidator处理密码强度、用户名唯一性等复杂逻辑。 - 嵌套结构校验:通过
type: 'object'和type: 'array'实现对象、数组的深度校验。 - Element UI 高级应用:结合
el-table和el-tree实现表格行、树形节点的动态校验,通过动态prop绑定和递归路径查找精准定位错误。
在实际项目中,建议根据业务复杂度选择合适的方案:
- 简单表单:直接使用 Element UI 内置校验。
- 复杂结构(如嵌套对象、数组):结合
async-validator实现深度校验。 - 表格/树形表单:通过动态
prop和递归路径查找,确保校验规则与数据结构严格匹配。
官方文档:
- async-validator GitHub
- Element UI 表单文档