UniApp 实现的表单验证与提交功能
前言
在移动端应用开发中,表单是用户与应用交互的重要媒介。一个好的表单不仅布局合理、使用方便,还应该具备完善的验证与提交功能,以确保用户输入的数据准确无误。本文将分享如何在 UniApp 中实现表单验证与提交功能,帮助你构建更加健壮的表单系统。
作为一个经常与表单打交道的开发者,我发现很多初学者往往忽视了表单验证的重要性,或者实现方式不够优雅。通过本文,希望能为你提供一些关于 UniApp 表单处理的实用技巧和最佳实践。
为什么需要表单验证?
想象一下,如果没有表单验证,用户可能会提交不完整或格式错误的数据:
- 手机号码少输一位或输入了字母
- 密码太简单,不符合安全要求
- 重要字段被遗漏
- 日期格式错误
- 上传的图片尺寸过大或格式不支持
这些问题不仅会导致后端数据处理错误,还会影响用户体验。因此,前端表单验证显得尤为重要。
UniApp 表单验证的实现方式
在 UniApp 中,有多种方式可以实现表单验证:
- 使用原生方法自行实现
- 使用第三方验证库(如 async-validator)
- 结合 uView 等 UI 框架使用内置验证功能
下面我们主要讨论前两种方式的实现。
方式一:自行实现表单验证
自行实现的优点是灵活、无需引入额外依赖,但需要自己编写各种验证规则和处理逻辑。
基本思路:
- 定义表单数据模型
- 编写验证规则函数
- 在提交前调用验证函数
- 根据验证结果决定是否提交
让我们看一个简单的注册表单验证示例:
export default {
data() {
return {
// 表单数据
form: {
username: '',
password: '',
confirmPassword: '',
mobile: '',
email: '',
agree: false
},
// 错误信息
errors: {
username: '',
password: '',
confirmPassword: '',
mobile: '',
email: '',
agree: ''
}
}
},
methods: {
// 验证用户名
validateUsername() {
if (!this.form.username) {
this.errors.username = '用户名不能为空';
return false;
}
if (this.form.username.length < 3 || this.form.username.length > 20) {
this.errors.username = '用户名长度应为3-20个字符';
return false;
}
this.errors.username = '';
return true;
},
// 验证密码
validatePassword() {
if (!this.form.password) {
this.errors.password = '密码不能为空';
return false;
}
if (this.form.password.length < 6) {
this.errors.password = '密码长度不能少于6个字符';
return false;
}
// 包含数字和字母的正则表达式
const passwordPattern = /^(?=.*[0-9])(?=.*[a-zA-Z]).{6,}$/;
if (!passwordPattern.test(this.form.password)) {
this.errors.password = '密码必须包含数字和字母';
return false;
}
this.errors.password = '';
return true;
},
// 验证确认密码
validateConfirmPassword() {
if (!this.form.confirmPassword) {
this.errors.confirmPassword = '请确认密码';
return false;
}
if (this.form.confirmPassword !== this.form.password) {
this.errors.confirmPassword = '两次输入的密码不一致';
return false;
}
this.errors.confirmPassword = '';
return true;
},
// 验证手机号
validateMobile() {
if (!this.form.mobile) {
this.errors.mobile = '手机号不能为空';
return false;
}
// 中国大陆手机号正则表达式
const mobilePattern = /^1[3-9]\d{9}$/;
if (!mobilePattern.test(this.form.mobile)) {
this.errors.mobile = '请输入有效的手机号码';
return false;
}
this.errors.mobile = '';
return true;
},
// 验证邮箱
validateEmail() {
if (!this.form.email) {
this.errors.email = '邮箱不能为空';
return false;
}
// 邮箱正则表达式
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailPattern.test(this.form.email)) {
this.errors.email = '请输入有效的邮箱地址';
return false;
}
this.errors.email = '';
return true;
},
// 验证用户协议
validateAgree() {
if (!this.form.agree) {
this.errors.agree = '请同意用户协议';
return false;
}
this.errors.agree = '';
return true;
},
// 验证所有字段
validateForm() {
const usernameValid = this.validateUsername();
const passwordValid = this.validatePassword();
const confirmPasswordValid = this.validateConfirmPassword();
const mobileValid = this.validateMobile();
const emailValid = this.validateEmail();
const agreeValid = this.validateAgree();
return usernameValid && passwordValid && confirmPasswordValid
&& mobileValid && emailValid && agreeValid;
},
// 提交表单
submitForm() {
// 先清空所有错误信息
for (let key in this.errors) {
this.errors[key] = '';
}
// 验证表单
if (!this.validateForm()) {
uni.showToast({
title: '请正确填写表单信息',
icon: 'none'
});
return;
}
// 验证通过,可以进行提交操作
uni.showLoading({
title: '提交中...'
});
// 模拟提交请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
// 重置表单或跳转页面
// this.resetForm();
// uni.navigateTo({ url: '/pages/login/login' });
}, 1500);
},
// 重置表单
resetForm() {
this.form = {
username: '',
password: '',
confirmPassword: '',
mobile: '',
email: '',
agree: false
};
for (let key in this.errors) {
this.errors[key] = '';
}
}
}
}
对应的模板部分可以这样编写:
<template>
<view class="register-form">
<view class="form-item">
<text class="label">用户名</text>
<input class="input" v-model="form.username" placeholder="请输入用户名" @blur="validateUsername" />
<text v-if="errors.username" class="error-tip">{{ errors.username }}</text>
</view>
<view class="form-item">
<text class="label">密码</text>
<input class="input" v-model="form.password" type="password" placeholder="请输入密码" @blur="validatePassword" />
<text v-if="errors.password" class="error-tip">{{ errors.password }}</text>
</view>
<view class="form-item">
<text class="label">确认密码</text>
<input class="input" v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" @blur="validateConfirmPassword" />
<text v-if="errors.confirmPassword" class="error-tip">{{ errors.confirmPassword }}</text>
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" v-model="form.mobile" type="number" placeholder="请输入手机号" @blur="validateMobile" />
<text v-if="errors.mobile" class="error-tip">{{ errors.mobile }}</text>
</view>
<view class="form-item">
<text class="label">邮箱</text>
<input class="input" v-model="form.email" placeholder="请输入邮箱" @blur="validateEmail" />
<text v-if="errors.email" class="error-tip">{{ errors.email }}</text>
</view>
<view class="form-item checkbox-item">
<checkbox v-model="form.agree" />
<text class="agreement-text" @click="form.agree = !form.agree">我已阅读并同意《用户协议》</text>
<text v-if="errors.agree" class="error-tip">{{ errors.agree }}</text>
</view>
<button class="submit-btn" type="primary" @click="submitForm">立即注册</button>
</view>
</template>
并添加一些基本样式:
<style scoped>
.register-form {
padding: 30rpx;
}
.form-item {
margin-bottom: 40rpx;
position: relative;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
}
.input {
width: 100%;
height: 80rpx;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 20rpx;
box-sizing: border-box;
font-size: 28rpx;
}
.error-tip {
position: absolute;
left: 0;
bottom: -36rpx;
font-size: 24rpx;
color: #f56c6c;
}
.checkbox-item {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 26rpx;
margin-left: 10rpx;
}
.submit-btn {
margin-top: 60rpx;
}
</style>
方式二:使用 async-validator 库
对于复杂的验证需求,使用成熟的验证库会更加便捷。async-validator 是一个流行的表单验证库,支持丰富的验证规则和自定义验证功能。
首先,安装依赖:
npm install async-validator --save
然后,在组件中使用:
import Schema from 'async-validator';
export default {
data() {
return {
// 表单数据
form: {
username: '',
password: '',
confirmPassword: '',
mobile: '',
email: '',
agree: false
},
// 错误信息
errors: {}
}
},
computed: {
// 定义验证规则
rules() {
return {
username: [
{ required: true, message: '用户名不能为空' },
{ min: 3, max: 20, message: '用户名长度应为3-20个字符' }
],
password: [
{ required: true, message: '密码不能为空' },
{ min: 6, message: '密码长度不能少于6个字符' },
{
pattern: /^(?=.*[0-9])(?=.*[a-zA-Z]).{6,}$/,
message: '密码必须包含数字和字母'
}
],
confirmPassword: [
{ required: true, message: '请确认密码' },
{
validator: (rule, value, callback) => {
if (value !== this.form.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
}
}
],
mobile: [
{ required: true, message: '手机号不能为空' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{ type: 'email', message: '请输入有效的邮箱地址' }
],
agree: [
{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请同意用户协议'));
} else {
callback();
}
}
}
]
};
}
},
methods: {
// 验证单个字段
validateField(field) {
const descriptor = {};
descriptor[field] = this.rules[field];
const validator = new Schema(descriptor);
const source = {};
source[field] = this.form[field];
validator.validate(source, (errors) => {
if (errors) {
// 有错误
this.$set(this.errors, field, errors[0].message);
} else {
// 没有错误
this.$set(this.errors, field, '');
}
});
},
// 验证所有字段
validateForm() {
return new Promise((resolve, reject) => {
const validator = new Schema(this.rules);
validator.validate(this.form, (errors) => {
if (errors) {
// 有错误
const errorObj = {};
errors.forEach(error => {
errorObj[error.field] = error.message;
});
this.errors = errorObj;
resolve(false);
} else {
// 验证通过
this.errors = {};
resolve(true);
}
});
});
},
// 提交表单
async submitForm() {
const valid = await this.validateForm();
if (!valid) {
uni.showToast({
title: '请正确填写表单信息',
icon: 'none'
});
return;
}
// 验证通过,可以进行提交操作
uni.showLoading({
title: '提交中...'
});
// 模拟提交请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
// 重置表单或跳转页面
// this.resetForm();
// uni.navigateTo({ url: '/pages/login/login' });
}, 1500);
},
// 重置表单
resetForm() {
this.form = {
username: '',
password: '',
confirmPassword: '',
mobile: '',
email: '',
agree: false
};
this.errors = {};
}
}
}
模板部分可以与前面的例子类似,只需要修改验证方法的调用:
<input class="input" v-model="form.username" placeholder="请输入用户名" @blur="validateField('username')" />
表单提交功能的实现
表单验证通过后,我们需要将数据提交到服务器。在 UniApp 中,可以使用 uni.request()
方法发送网络请求。
下面是一个完整的表单提交示例:
// 提交表单
async submitForm() {
const valid = await this.validateForm();
if (!valid) {
uni.showToast({
title: '请正确填写表单信息',
icon: 'none'
});
return;
}
// 显示加载提示
uni.showLoading({
title: '提交中...'
});
try {
// 发送请求
const res = await uni.request({
url: 'https://api.example.com/register',
method: 'POST',
data: this.form,
header: {
'content-type': 'application/json'
}
});
// 请求成功
if (res.statusCode === 200 && res.data.code === 0) {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
// 存储登录信息
uni.setStorageSync('token', res.data.data.token);
uni.setStorageSync('userInfo', res.data.data.userInfo);
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 1500);
} else {
throw new Error(res.data.message || '注册失败');
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
});
}
}
实战案例:会员信息编辑表单
下面是一个完整的会员信息编辑表单案例,综合了表单验证和提交功能:
<template>
<view class="profile-form">
<view class="form-header">
<view class="avatar-wrapper">
<image class="avatar" :src="form.avatar || '/static/default-avatar.png'" @click="chooseAvatar"></image>
<text class="edit-hint">点击修改头像</text>
</view>
</view>
<view class="form-content">
<view class="form-item">
<text class="label">姓名</text>
<input class="input" v-model="form.name" placeholder="请输入您的姓名" @blur="validateField('name')" />
<text v-if="errors.name" class="error-tip">{{ errors.name }}</text>
</view>
<view class="form-item">
<text class="label">性别</text>
<view class="radio-group">
<view class="radio-item" @click="form.gender = 1">
<view class="radio-box" :class="{ 'checked': form.gender === 1 }"></view>
<text class="radio-label">男</text>
</view>
<view class="radio-item" @click="form.gender = 2">
<view class="radio-box" :class="{ 'checked': form.gender === 2 }"></view>
<text class="radio-label">女</text>
</view>
</view>
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" v-model="form.mobile" type="number" placeholder="请输入手机号" @blur="validateField('mobile')" />
<text v-if="errors.mobile" class="error-tip">{{ errors.mobile }}</text>
</view>
<view class="form-item">
<text class="label">邮箱</text>
<input class="input" v-model="form.email" placeholder="请输入邮箱" @blur="validateField('email')" />
<text v-if="errors.email" class="error-tip">{{ errors.email }}</text>
</view>
<view class="form-item">
<text class="label">生日</text>
<picker mode="date" :value="form.birthday" @change="onBirthdayChange">
<view class="picker-box">
<text class="picker-text">{{ form.birthday || '请选择出生日期' }}</text>
<text class="picker-arrow">></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">地址</text>
<textarea class="textarea" v-model="form.address" placeholder="请输入您的详细地址" @blur="validateField('address')" />
<text v-if="errors.address" class="error-tip">{{ errors.address }}</text>
</view>
<view class="form-item">
<text class="label">个人简介</text>
<textarea class="textarea" v-model="form.bio" placeholder="介绍一下自己吧(选填)" />
</view>
</view>
<button class="submit-btn" type="primary" @click="submitForm">保存修改</button>
</view>
</template>
<script>
import Schema from 'async-validator';
export default {
data() {
return {
// 表单数据
form: {
avatar: '',
name: '',
gender: 1, // 1-男,2-女
mobile: '',
email: '',
birthday: '',
address: '',
bio: ''
},
// 错误信息
errors: {}
}
},
computed: {
// 定义验证规则
rules() {
return {
name: [
{ required: true, message: '姓名不能为空' },
{ max: 20, message: '姓名长度不能超过20个字符' }
],
mobile: [
{ required: true, message: '手机号不能为空' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{ type: 'email', message: '请输入有效的邮箱地址' }
],
address: [
{ required: true, message: '地址不能为空' },
{ max: 100, message: '地址长度不能超过100个字符' }
]
};
}
},
onLoad() {
// 获取用户信息(示例数据)
const userInfo = {
avatar: '/static/avatar.png',
name: '张三',
gender: 1,
mobile: '13800138000',
email: 'zhangsan@example.com',
birthday: '1990-01-01',
address: '北京市朝阳区某某街道某某小区',
bio: '热爱生活,热爱编程。'
};
// 填充表单
this.form = { ...userInfo };
},
methods: {
// 选择头像
chooseAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths;
this.form.avatar = tempFilePaths[0];
// 实际场景中应该先上传图片,再获取图片的URL
this.uploadAvatar(tempFilePaths[0]);
}
});
},
// 上传头像
uploadAvatar(filePath) {
uni.showLoading({
title: '上传中...'
});
// 模拟上传
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '头像上传成功',
icon: 'success'
});
// 实际场景中,这里应该返回服务器的图片URL
// this.form.avatar = res.data.url;
}, 1500);
},
// 生日改变
onBirthdayChange(e) {
this.form.birthday = e.detail.value;
},
// 验证单个字段
validateField(field) {
if (!this.rules[field]) return;
const descriptor = {};
descriptor[field] = this.rules[field];
const validator = new Schema(descriptor);
const source = {};
source[field] = this.form[field];
validator.validate(source, (errors) => {
if (errors) {
// 有错误
this.$set(this.errors, field, errors[0].message);
} else {
// 没有错误
this.$set(this.errors, field, '');
}
});
},
// 验证所有字段
validateForm() {
return new Promise((resolve, reject) => {
const validator = new Schema(this.rules);
validator.validate(this.form, (errors) => {
if (errors) {
// 有错误
const errorObj = {};
errors.forEach(error => {
errorObj[error.field] = error.message;
});
this.errors = errorObj;
resolve(false);
} else {
// 验证通过
this.errors = {};
resolve(true);
}
});
});
},
// 提交表单
async submitForm() {
const valid = await this.validateForm();
if (!valid) {
uni.showToast({
title: '请正确填写表单信息',
icon: 'none'
});
return;
}
// 显示加载提示
uni.showLoading({
title: '保存中...'
});
// 模拟提交请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1500);
/* 实际场景中的提交代码
try {
const res = await uni.request({
url: 'https://api.example.com/update-profile',
method: 'POST',
data: this.form,
header: {
'content-type': 'application/json',
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
if (res.statusCode === 200 && res.data.code === 0) {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 更新本地存储的用户信息
uni.setStorageSync('userInfo', res.data.data.userInfo);
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
throw new Error(res.data.message || '保存失败');
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || '网络错误,请稍后重试',
icon: 'none'
});
}
*/
}
}
}
</script>
<style scoped>
.profile-form {
padding: 30rpx;
}
.form-header {
display: flex;
justify-content: center;
margin-bottom: 50rpx;
}
.avatar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 150rpx;
height: 150rpx;
border-radius: 50%;
margin-bottom: 10rpx;
}
.edit-hint {
font-size: 24rpx;
color: #666;
}
.form-content {
margin-bottom: 40rpx;
}
.form-item {
margin-bottom: 40rpx;
position: relative;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
}
.input, .textarea, .picker-box {
width: 100%;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 20rpx;
box-sizing: border-box;
font-size: 28rpx;
}
.input, .picker-box {
height: 80rpx;
line-height: 80rpx;
}
.textarea {
height: 160rpx;
padding: 20rpx;
line-height: 1.5;
}
.picker-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
color: #333;
}
.picker-arrow {
color: #999;
transform: rotate(90deg);
}
.radio-group {
display: flex;
margin-top: 10rpx;
}
.radio-item {
display: flex;
align-items: center;
margin-right: 50rpx;
}
.radio-box {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
position: relative;
}
.radio-box.checked {
border-color: #2979ff;
}
.radio-box.checked:after {
content: '';
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #2979ff;
position: absolute;
}
.radio-label {
margin-left: 10rpx;
font-size: 28rpx;
}
.error-tip {
position: absolute;
left: 0;
bottom: -36rpx;
font-size: 24rpx;
color: #f56c6c;
}
.submit-btn {
margin-top: 60rpx;
}
</style>
表单验证的最佳实践
- 实时验证与提交验证结合:在输入框失去焦点时进行单个字段验证,在表单提交时进行全表单验证
- 友好的错误提示:错误信息应该清晰明了,位置合适
- 良好的用户体验:添加适当的过渡效果,不要让错误提示突兀出现
- 避免重复验证:已验证过且符合要求的字段无需重复验证
- 表单防抖:防止用户频繁点击提交按钮
- 状态保持:表单提交失败后不要清空用户输入
- 进度提示:使用加载提示,让用户知道表单正在提交
总结
本文介绍了在 UniApp 中实现表单验证与提交功能的多种方式,包括自定义验证和使用第三方库验证。我们通过实例详细讲解了各种验证规则的编写,以及表单提交的完整流程。
表单验证看似简单,但实际上涉及众多细节,一个设计良好的表单验证系统能极大提升用户体验。希望本文对你在 UniApp 中开发表单功能有所帮助。
在实际项目中,你可能需要根据业务需求进行更多定制化的开发,例如添加更复杂的验证规则、优化表单的交互效果、处理更多的表单场景等。不论如何变化,本文提供的基本思路和方法都是适用的。
进一步思考
- 如何处理更复杂的表单依赖验证?(例如,当选择A选项时,B字段必填)
- 如何处理文件上传类型的表单字段?
- 如何优化大型复杂表单的性能?
- 如何实现多步骤表单?
这些都是表单开发中的进阶话题,欢迎在实践中探索更多解决方案。