在这里插入图片描述

摘要:表单验证是移动应用开发中最常见的需求之一。本文深入讲解如何在 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 })
  }
}

组件亮点

  1. @Link 双向绑定valueerror 使用 @Link 装饰器,实现子组件与父组件数据的双向同步
  2. 实时校验:在 onChange 回调中触发验证,用户每输入一个字符都能即时获得反馈
  3. 条件渲染:错误信息通过 if (this.error.length > 0) 控制显示/隐藏,减少视觉噪点
  4. 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 中接收 valueerror
@Prop 单向数据传递 标签文本、占位符等只读属性

@Link 关键要点

  • 父组件传入的变量必须是 @State 类型
  • 子组件修改 @Link 变量会同步更新父组件
  • 避免了回调函数的繁琐传递,代码更简洁

4.2 Scroll 组件处理长表单

当表单字段较多时,使用 Scroll 组件包裹解决键盘遮挡问题:

Scroll() {
  Column({ space: 8 }) {
    // ... 所有表单字段
  }
  .width('92%')
}
.scrollable(ScrollDirection.Vertical)

最佳实践

  • 设置 edgeEffectEdgeEffect.Spring 提供弹性滚动效果
  • 使用 scrollBar 控制滚动条显示
  • 键盘弹出时自动滚动到当前焦点字段

4.3 InputType 输入类型

ArkTS 的 TextInput 组件支持多种输入类型:

TextInput({ placeholder: '请输入手机号' })
  .type(InputType.Number)      // 数字键盘
  .maxLength(11)               // 限制最大长度

TextInput({ placeholder: '请输入密码' })
  .type(InputType.Password)    // 密码模式,显示为圆点
InputType 适用场景
Normal 普通文本
Number 数字输入(手机号、验证码)
Password 密码输入
Email 邮箱输入(自动补全 @ 后缀)

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 安全性考虑

  1. 前端验证≠安全验证:前端验证只用于提升用户体验,后端必须重新验证所有数据
  2. 避免敏感信息暴露:错误提示不要泄露用户是否存在(如「该邮箱已注册」改为「注册信息已提交」)
  3. 防止 XSS:对用户输入进行转义处理,避免直接渲染 HTML

6.2 性能优化

  1. 防抖处理:对于即时校验,使用防抖函数减少频繁验证(尤其是网络校验)
  2. 懒校验:字段未聚焦时不做校验,减少不必要计算
  3. 规则缓存:将规则数组定义为静态常量,避免每次校验时重新创建
// 防抖装饰器示例
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 测试建议

  1. 单元测试验证器:对每个正则表达式编写边界测试用例
  2. UI 测试:使用 onChange 模拟输入,验证错误提示是否正确显示
  3. 集成测试:验证表单提交时各字段联动校验逻辑

七、扩展与演进

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 表单状态持久化

使用 AppStoragePersistentStorage 在页面间保持表单数据:

@StorageLink('draftUsername') draftUsername: string = ''

八、结语

表单验证看似简单,实则蕴含着架构设计、用户体验、安全防护等多方面的考量。通过本项目的学习,我们掌握了:

  1. 正则表达式在 ArkTS 中的实际应用
  2. @State / @Link 装饰器实现响应式数据流
  3. 组件化思想构建可复用的表单字段
  4. 即时校验集中校验相结合的验证策略
  5. HarmonyOS TextInput 组件的高级用法

这些知识不仅适用于表单验证,更能推广到任何需要输入校验的场景中。掌握好这些基础,就能在更复杂的应用中游刃有余。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐