HarmonyOS ArkTS 表单验证实战 :正则表达式与输入校验

摘要:表单验证是移动应用开发中最常见的需求之一。本文深入讲解如何在 HarmonyOS ArkTS 中构建健壮的表单验证系统,涵盖正则表达式(邮箱、11位手机号)、用户名长度校验、密码一致性验证以及错误信息实时显示等完整技术栈。
一、项目概述
「表单验证」模块是每个应用中不可或缺的组成部分。无论是登录、注册、信息填写还是支付流程,可靠的输入校验都是保障数据质量和用户体验的关键防线。
本项目中我们将实现一个完整的注册表单,包含以下校验规则:
| 字段 | 验证规则 | 错误提示 |
|---|---|---|
| 用户名 | 4-20 个字符,仅允许字母、数字、下划线 | 用户名长度不符或包含非法字符 |
| 邮箱 | 标准邮箱格式正则匹配 | 请输入有效的邮箱地址 |
| 手机号 | 11 位数字,以 1 开头 | 请输入正确的 11 位手机号 |
| 密码 | 不少于 8 位,需含字母和数字 | 密码强度不足 |
| 确认密码 | 与密码字段完全一致 | 两次输入的密码不一致 |
二、项目架构
entry/src/main/ets/
├── pages/
│ └── FormValidation.ets // 表单主页面
├── utils/
│ ├── Validators.ets // 正则验证器集合
│ └── ValidationRules.ets // 校验规则定义
└── components/
└── FormField.ets // 可复用的表单字段组件
设计理念:
- 职责单一:每个文件只负责一个层面的功能
- 可复用性:
FormField组件可以在任意表单中复用 - 可扩展性:
Validators中新增验证函数即可扩展规则 - 声明式 UI:错误信息通过状态驱动自动显示/隐藏
三、核心技术分析
3.1 正则表达式验证器
正则表达式是表单验证的核心武器。我们封装一个独立的验证器模块:
// utils/Validators.ets
export class Validators {
// 邮箱验证:标准邮箱格式
static isValidEmail(email: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email)
}
// 手机号验证:11 位数字,以 1 开头
static isValidPhone(phone: string): boolean {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
// 用户名验证:4-20 位字母、数字、下划线
static isValidUsername(username: string): boolean {
const usernameRegex = /^[a-zA-Z0-9_]{4,20}$/
return usernameRegex.test(username)
}
// 密码强度验证:至少 8 位,包含字母和数字
static isValidPassword(password: string): boolean {
const hasLetter = /[a-zA-Z]/.test(password)
const hasNumber = /\d/.test(password)
return password.length >= 8 && hasLetter && hasNumber
}
// 确认密码验证
static isPasswordMatch(password: string, confirmPassword: string): boolean {
return password === confirmPassword
}
}
正则详解:
| 正则 | 说明 |
|---|---|
/^1[3-9]\d{9}$/ |
以 1 开头,第二位 3-9,后跟 9 位数字,共 11 位 |
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ |
标准邮箱,支持点号、下划线、百分号等特殊字符 |
/^[a-zA-Z0-9_]{4,20}$/ |
只允许字母、数字、下划线,4-20 位 |
//[a-zA-Z]/.test(pwd) |
检查是否包含至少一个字母 |
3.2 验证规则引擎
每条验证规则作为一个独立对象,包含验证函数和错误消息:
// utils/ValidationRules.ets
import { Validators } from './Validators'
export interface ValidationRule {
validate: (value: string) => boolean
errorMessage: string
}
export class ValidationRules {
static getUsernameRules(): ValidationRule[] {
return [
{
validate: (value: string) => value.length >= 4 && value.length <= 20,
errorMessage: '用户名长度必须在 4-20 个字符之间'
},
{
validate: (value: string) => /^[a-zA-Z0-9_]+$/.test(value),
errorMessage: '用户名只能包含字母、数字和下划线'
}
]
}
static getEmailRules(): ValidationRule[] {
return [
{
validate: (value: string) => value.length > 0,
errorMessage: '邮箱地址不能为空'
},
{
validate: (value: string) => Validators.isValidEmail(value),
errorMessage: '请输入有效的邮箱地址(如 user@example.com)'
}
]
}
static getPhoneRules(): ValidationRule[] {
return [
{
validate: (value: string) => value.length === 11,
errorMessage: '手机号必须为 11 位'
},
{
validate: (value: string) => /^1\d{10}$/.test(value),
errorMessage: '手机号必须以 1 开头'
},
{
validate: (value: string) => Validators.isValidPhone(value),
errorMessage: '请输入正确的手机号格式'
}
]
}
static getPasswordRules(): ValidationRule[] {
return [
{
validate: (value: string) => value.length >= 8,
errorMessage: '密码长度不能少于 8 位'
},
{
validate: (value: string) => /[a-zA-Z]/.test(value) && /\d/.test(value),
errorMessage: '密码必须包含字母和数字'
}
]
}
}
这种 规则链模式 的优势是:
- 每条规则独立,可单独调试
- 按顺序验证,遇到第一条失败的规则即返回对应错误
- 新增规则只需添加数组元素,无需修改逻辑
3.3 可复用表单字段组件
为每个表单项创建一个独立组件,封装标签、输入框和错误显示:
// components/FormField.ets
@Component
export struct FormField {
private label: string = ''
private placeholder: string = ''
private type: InputType = InputType.Normal
@Link value: string
@Link error: string
private onValidate?: () => void
build() {
Column({ space: 6 }) {
// 标签
Text(this.label)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
// 输入框
TextInput({ placeholder: this.placeholder, text: this.value })
.type(this.type)
.width('100%')
.height(48)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.onChange((val: string) => {
this.value = val
if (this.onValidate) {
this.onValidate()
}
})
// 错误提示
if (this.error.length > 0) {
Text(this.error)
.fontSize(13)
.fontColor(Color.Red)
.width('100%')
.margin({ top: 2 })
}
}
.width('100%')
.margin({ bottom: 16 })
}
}
组件亮点:
- @Link 双向绑定:
value和error使用@Link装饰器,实现子组件与父组件数据的双向同步 - 实时校验:在
onChange回调中触发验证,用户每输入一个字符都能即时获得反馈 - 条件渲染:错误信息通过
if (this.error.length > 0)控制显示/隐藏,减少视觉噪点 - InputType 支持:密码字段使用
InputType.Password,手机号使用InputType.Number
3.4 主表单页面整合
在主页面上组装所有表单字段,并实现整体校验逻辑:
// pages/FormValidation.ets
import { FormField } from '../components/FormField'
import { ValidationRules } from '../utils/ValidationRules'
@Entry
@Component
struct FormValidationPage {
@State username: string = ''
@State email: string = ''
@State phone: string = ''
@State password: string = ''
@State confirmPassword: string = ''
@State usernameError: string = ''
@State emailError: string = ''
@State phoneError: string = ''
@State passwordError: string = ''
@State confirmPasswordError: string = ''
@State isSubmitting: boolean = false
@State submitSuccess: boolean = false
// 验证单个字段
validateField(
value: string,
rules: { validate: (v: string) => boolean; errorMessage: string }[]
): string {
if (value.length === 0) {
return '此项不能为空'
}
for (const rule of rules) {
if (!rule.validate(value)) {
return rule.errorMessage
}
}
return ''
}
// 验证用户名
validateUsername(): void {
this.usernameError = this.validateField(
this.username, ValidationRules.getUsernameRules()
)
}
// 验证邮箱
validateEmail(): void {
this.emailError = this.validateField(
this.email, ValidationRules.getEmailRules()
)
}
// 验证手机号
validatePhone(): void {
this.phoneError = this.validateField(
this.phone, ValidationRules.getPhoneRules()
)
}
// 验证密码
validatePassword(): void {
this.passwordError = this.validateField(
this.password, ValidationRules.getPasswordRules()
)
// 如果确认密码已有值,联动校验
if (this.confirmPassword.length > 0) {
this.validateConfirmPassword()
}
}
// 验证确认密码
validateConfirmPassword(): void {
if (this.confirmPassword !== this.password) {
this.confirmPasswordError = '两次输入的密码不一致'
} else {
this.confirmPasswordError = this.validateField(
this.confirmPassword, [
{
validate: (v: string) => v === this.password,
errorMessage: '两次输入的密码不一致'
}
]
)
}
}
// 全量校验
validateAll(): boolean {
this.validateUsername()
this.validateEmail()
this.validatePhone()
this.validatePassword()
this.validateConfirmPassword()
return (
this.usernameError.length === 0 &&
this.emailError.length === 0 &&
this.phoneError.length === 0 &&
this.passwordError.length === 0 &&
this.confirmPasswordError.length === 0 &&
this.username.length > 0 &&
this.email.length > 0 &&
this.phone.length > 0 &&
this.password.length > 0 &&
this.confirmPassword.length > 0
)
}
// 提交表单
submitForm(): void {
this.isSubmitting = true
if (this.validateAll()) {
// 模拟提交
setTimeout(() => {
this.submitSuccess = true
this.isSubmitting = false
}, 1500)
} else {
this.isSubmitting = false
}
}
build() {
Scroll() {
Column({ space: 8 }) {
// 标题
Text('用户注册')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.width('100%')
.margin({ top: 30, bottom: 10 })
Text('请填写以下信息完成注册')
.fontSize(14)
.fontColor('#666666')
.width('100%')
.margin({ bottom: 24 })
// 表单字段
FormField({
label: '用户名',
placeholder: '4-20 位字母、数字或下划线',
value: this.username,
error: this.usernameError,
onValidate: () => this.validateUsername()
})
FormField({
label: '邮箱',
placeholder: '请输入邮箱地址',
value: this.email,
error: this.emailError,
onValidate: () => this.validateEmail()
})
FormField({
label: '手机号',
placeholder: '请输入 11 位手机号',
type: InputType.Number,
value: this.phone,
error: this.phoneError,
onValidate: () => this.validatePhone()
})
FormField({
label: '密码',
placeholder: '不少于 8 位,含字母和数字',
type: InputType.Password,
value: this.password,
error: this.passwordError,
onValidate: () => this.validatePassword()
})
FormField({
label: '确认密码',
placeholder: '请再次输入密码',
type: InputType.Password,
value: this.confirmPassword,
error: this.confirmPasswordError,
onValidate: () => this.validateConfirmPassword()
})
// 提交按钮
Button(this.isSubmitting ? '提交中...' : '注册')
.width('100%')
.height(48)
.backgroundColor('#007AFF')
.fontColor(Color.White)
.borderRadius(8)
.margin({ top: 16 })
.disabled(this.isSubmitting)
.onClick(() => this.submitForm())
// 成功提示
if (this.submitSuccess) {
Text('✅ 注册成功!欢迎加入!')
.fontSize(16)
.fontColor(Color.Green)
.margin({ top: 16 })
}
}
.width('92%')
.margin({ left: '4%', right: '4%' })
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
四、HarmonyOS 特性深度解析
4.1 @State 与 @Link 装饰器
在表单验证中,装饰器的使用至关重要:
| 装饰器 | 作用 | 在本项目中的应用 |
|---|---|---|
@State |
标记组件内的响应式状态 | 表单字段值、错误信息、提交状态 |
@Link |
建立父子组件间的双向绑定 | FormField 中接收 value 和 error |
@Prop |
单向数据传递 | 标签文本、占位符等只读属性 |
@Link 关键要点:
- 父组件传入的变量必须是
@State类型 - 子组件修改
@Link变量会同步更新父组件 - 避免了回调函数的繁琐传递,代码更简洁
4.2 Scroll 组件处理长表单
当表单字段较多时,使用 Scroll 组件包裹解决键盘遮挡问题:
Scroll() {
Column({ space: 8 }) {
// ... 所有表单字段
}
.width('92%')
}
.scrollable(ScrollDirection.Vertical)
最佳实践:
- 设置
edgeEffect为EdgeEffect.Spring提供弹性滚动效果 - 使用
scrollBar控制滚动条显示 - 键盘弹出时自动滚动到当前焦点字段
4.3 InputType 输入类型
ArkTS 的 TextInput 组件支持多种输入类型:
TextInput({ placeholder: '请输入手机号' })
.type(InputType.Number) // 数字键盘
.maxLength(11) // 限制最大长度
TextInput({ placeholder: '请输入密码' })
.type(InputType.Password) // 密码模式,显示为圆点
| InputType | 适用场景 |
|---|---|
| Normal | 普通文本 |
| Number | 数字输入(手机号、验证码) |
| Password | 密码输入 |
| 邮箱输入(自动补全 @ 后缀) |
4.4 条件渲染控制错误显示
ArkTS 支持在 build 方法中使用条件语句:
if (this.error.length > 0) {
Text(this.error)
.fontSize(13)
.fontColor(Color.Red)
}
这种方式比 CSS 的 display:none 更高效,因为不满足条件时组件完全不会被创建,减少了 DOM 节点数。
五、UI/UX 设计要点
5.1 即时反馈 vs 提交时校验
两种校验时机的对比:
| 策略 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 即时校验(onChange) | 用户体验好,立即知道输入是否正确 | 性能开销略大 | 短表单、关键字段 |
| 提交时集中校验 | 性能好,减少频繁计算 | 用户可能最后看到一堆错误 | 长表单 |
建议:混合使用。单个字段失焦时校验,提交时全量校验。
5.2 错误信息的表达
好的错误提示应遵循 ETS 原则:
- Exact(准确):明确指出哪里错了
- Timely(及时):输入后立即反馈
- Specific(具体):告诉用户如何修正
❌ 不好的提示:「输入错误」
✅ 好的提示:「密码长度不能少于 8 位,且必须包含至少一个字母和一个数字」
5.3 视觉层次设计
┌─────────────────────────┐
│ 用户注册 (28px Bold) │ ← 主标题
│ 请填写以下信息完成注册 │ ← 副标题 (14px #666)
├─────────────────────────┤
│ 用户名 │ ← 字段标签 (16px Medium)
│ ┌─────────────────────┐ │
│ │ 4-20 位字母... │ │ ← 输入框 (48px height)
│ └─────────────────────┘ │
│ 用户名不能为空 │ ← 错误提示 (13px Red)
├─────────────────────────┤
│ 邮箱 │
│ ... │
├─────────────────────────┤
│ [注 册] │ ← 主按钮 (#007AFF)
└─────────────────────────┘
六、最佳实践总结
6.1 安全性考虑
- 前端验证≠安全验证:前端验证只用于提升用户体验,后端必须重新验证所有数据
- 避免敏感信息暴露:错误提示不要泄露用户是否存在(如「该邮箱已注册」改为「注册信息已提交」)
- 防止 XSS:对用户输入进行转义处理,避免直接渲染 HTML
6.2 性能优化
- 防抖处理:对于即时校验,使用防抖函数减少频繁验证(尤其是网络校验)
- 懒校验:字段未聚焦时不做校验,减少不必要计算
- 规则缓存:将规则数组定义为静态常量,避免每次校验时重新创建
// 防抖装饰器示例
function debounce(delay: number) {
let timer: number = -1
return (target: any, key: string, descriptor: PropertyDescriptor) => {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
clearTimeout(timer)
timer = setTimeout(() => original.apply(this, args), delay)
}
return descriptor
}
}
6.3 代码组织
- 验证逻辑与 UI 分离:Validators 模块只负责纯逻辑,不依赖任何 UI 组件
- 错误信息集中管理:所有错误提示字符串定义为常量,便于统一修改
- 组件粒度控制:
FormField组件封装了标签+输入+错误的完整结构,可在全项目复用
6.4 测试建议
- 单元测试验证器:对每个正则表达式编写边界测试用例
- UI 测试:使用
onChange模拟输入,验证错误提示是否正确显示 - 集成测试:验证表单提交时各字段联动校验逻辑
七、扩展与演进
7.1 国际化支持
将错误消息提取为资源文件,支持多语言:
// 资源文件 values/strings.json
{
"username_error_length": "Username must be 4-20 characters",
"phone_error_format": "Please enter a valid 11-digit phone number"
}
7.2 异步验证
支持远程校验(如检查用户名是否已注册):
async validateUsernameRemote(username: string): Promise<string> {
try {
const response = await fetch(`/api/check-username?name=${username}`)
const data = await response.json()
return data.exists ? '该用户名已被注册' : ''
} catch {
return '网络错误,请稍后重试'
}
}
7.3 表单状态持久化
使用 AppStorage 或 PersistentStorage 在页面间保持表单数据:
@StorageLink('draftUsername') draftUsername: string = ''
八、结语
表单验证看似简单,实则蕴含着架构设计、用户体验、安全防护等多方面的考量。通过本项目的学习,我们掌握了:
- 正则表达式在 ArkTS 中的实际应用
- @State / @Link 装饰器实现响应式数据流
- 组件化思想构建可复用的表单字段
- 即时校验与集中校验相结合的验证策略
- HarmonyOS TextInput 组件的高级用法
这些知识不仅适用于表单验证,更能推广到任何需要输入校验的场景中。掌握好这些基础,就能在更复杂的应用中游刃有余。
更多推荐



所有评论(0)