鸿蒙(HarmonyOS NEXT)表单校验别再手撸正则了 —— 我写了个 ArkTS 版 zod

鸿蒙(HarmonyOS NEXT)表单校验别再手撸正则了 ------ 我写了个 ArkTS 版 zod

一个零依赖、纯 ArkTS 的声明式校验库,内置手机号/身份证/银行卡等中国本地化规则 ,还能跟 ArkUI 表单组件实时联动。ohpm i @hmkit/validator 即用。

先说痛点

在鸿蒙上写一个注册表单,你大概率写过这样的代码:

kotlin 复制代码
// 校验手机号
if (!phone) {
  this.phoneErr = '请输入手机号';
} else if (!/^1[3-9]\d{9}$/.test(phone)) {
  this.phoneErr = '手机号格式不正确';
} else {
  this.phoneErr = '';
}
​
// 校验身份证...校验位算法你还记得吗?
// 校验银行卡...Luhn 算法又得查一遍
// 校验年龄、邮箱、确认密码...一个表单 100 行 if 起步

问题很明显:

  • 重复 :每个字段一坨 if/else,十个字段十坨。
  • 易错:身份证最后一位校验码、银行卡 Luhn,自己撸十有八九写错。
  • 难维护:校验逻辑散落在 UI 里,改个规则要翻半天。

前端有 zod/yup,后端有各种 validator,鸿蒙生态却一直缺一个顺手的 。于是我把这件事做了:@hmkit/validator

Before / After

同样校验「手机号 + 身份证 + 年龄满 18」:

Before(手撸,~30 行)

typescript 复制代码
let errors: string[] = [];
if (!phone) errors.push('请输入手机号');
else if (!/^1[3-9]\d{9}$/.test(phone)) errors.push('手机号格式不正确');
​
if (!idCard) errors.push('请输入身份证');
else if (!isValidIdCard(idCard)) errors.push('身份证不正确'); // isValidIdCard 还得自己写校验位
​
if (age === undefined) errors.push('请输入年龄');
else if (!Number.isInteger(age)) errors.push('年龄须为整数');
else if (age < 18) errors.push('需年满 18 岁');
// ...

After(链式声明,一目了然)

css 复制代码
import { v } from '@hmkit/validator';
​
// 注意:ArkTS 严格模式要求 Record 字面量 key 加引号
const schema = v.object({
  'phone':  v.string().required('请输入手机号').phone(),
  'idCard': v.string().required().idCard(),
  'age':    v.number().required().integer().min(18, '需年满 18 岁'),
});
​
const result = schema.validate({ 'phone': phone, 'idCard': idCard, 'age': age });
// result.errors: [{ path: 'phone', message: '请输入正确的手机号' }, ...]

校验逻辑变成声明,错误自带字段路径,读起来像说话。

一分钟上手

bash 复制代码
ohpm install @hmkit/validator
lua 复制代码
import { v } from '@hmkit/validator';
​
// 单值
v.string().required().phone().validate('13800138000'); // { valid: true, errors: [] }
​
// 自定义错误信息
v.number().min(18, '需年满 18 岁').validate(16);
// { valid: false, errors: [{ path: '', message: '需年满 18 岁' }] }

核心能力

🇨🇳 中国本地化规则,开箱即用

这是它和「移植个 zod」最不一样的地方 ------ 国内高频场景一个库全搞定,而且该有的校验算法都做对了:

规则 说明
phone() 手机号
idCard() 18 位身份证(校验出生日期 + 校验位)
bankCard() 银行卡(Luhn 算法)
creditCode() 统一社会信用代码(GB32100 校验位)
plateNumber() 车牌(含新能源)
landline() postalCode() 固话 / 邮编
chineseName() qq() wechat() 中文姓名 / QQ / 微信号
vin() ipv4() url() email() 车架号 / IPv4 / 网址 / 邮箱

身份证不光算校验位,还会拒掉 1900-02-31 这种假日期;信用代码也补了 mod-31 校验位 ------ 这些细节才是「能用在生产」和「demo」的区别。

🧩 数组、深层嵌套、跨字段

css 复制代码
// 数组逐元素校验,错误路径带下标
v.array(v.string().phone()).validate(['13800138000', '123']);
// errors: [{ path: '1', message: '请输入正确的手机号' }]
​
// 对象 ↔ 数组任意层级嵌套,路径自动拼接成 items.0.name
v.object({
  'items': v.array(v.object({ 'name': v.string().required('请输入商品名') })),
}).validate({ 'items': [{ 'name': '' }] });
// errors: [{ path: 'items.0.name', message: '请输入商品名' }]
​
// 跨字段:确认密码一致
v.object({ 'pwd': v.string().required(), 'confirm': v.string().required() })
  .refine(o => o['pwd'] === o['confirm'], '两次密码不一致', 'confirm');

⏳ 异步校验(远程查重)

用户名/手机号是否已注册,直接接异步规则:

dart 复制代码
const schema = v.string().required().customAsync(
  async (name) => await api.isUsernameAvailable(name), // 返回 true 通过
  '用户名已被占用',
);
await schema.validateAsync('taken'); // { valid: false, errors: [...] }

🎚️ 条件 / 可选

ini 复制代码
// 企业(type=company)才必填税号
v.string().requiredWhen('type', t => t === 'company', '企业必须填写税号');

招牌:跟 ArkUI 表单实时联动

光有校验逻辑还不够,鸿蒙开发者最想要的是「输入时实时变红、提交时整体校验」。库里带了一个 FormValidator 控制器,跟 TextInput 一接就是完整体验:

typescript 复制代码
import { v, FormValidator } from '@hmkit/validator';

@Entry
@Component
struct RegisterForm {
  private validator: FormValidator = new FormValidator({
    'username': v.string().required('请输入用户名').min(3, '用户名至少3位'),
    'phone':    v.string().required().phone(),
  });

  @State username: string = '';
  @State errors: Record<string, string> = {};

  build() {
    Column() {
      // 关键:text 用 $$ 双向绑定,输入不丢焦点
      TextInput({ text: $$this.username, placeholder: '用户名' })
        .onChange((val: string) => {
          const msg = this.validator.validateField('username', val);
          this.errors = msg === null ? {} as Record<string, string>
                                     : { 'username': msg } as Record<string, string>;
        })
      if (this.errors['username'] !== undefined) {
        Text(this.errors['username']).fontColor('#E64340')
      }

      Button('提交').onClick(() => {
        const errs = this.validator.validateAll({ 'username': this.username });
        this.errors = errs; // 提交时整体校验
      })
    }
  }
}

输入时变红框、提交时整体校验,一套搞定。

为什么敢用在生产

  • 零依赖、纯 ArkTS,只想要校验逻辑的人不被迫拉任何 UI 依赖。
  • 190 项单元测试覆盖每个规则 + 边界 + 反例(假身份证日期、错校验位、空值、跨字段、异步...),全过。
  • 校验算法对照国标实现(身份证 GB11643、信用代码 GB32100、银行卡 Luhn)。
  • 同步 API 向后兼容,异步规则按需启用。

链接

如果你也在鸿蒙上写表单,不妨试试,少撸几行正则。觉得有用点个 star,有需求提 issue,我会持续迭代 🙌

相关推荐
TrisighT2 小时前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
ONEDAY1 天前
HarmonyOS 多 Product 构建实践:一套代码生成多个产物
harmonyos
TT_Close1 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT1 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
MonkeyKing1 天前
鸿蒙ArkTS深度剖析:ArkTS与TS/JS核心差异、静态强类型实战优势
typescript·harmonyos
TrisighT1 天前
Electron鸿蒙PC上写日志文件,我被权限和路径坑了两次
electron·harmonyos
TrisighT2 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
花椒技术5 天前
HJPusher / HJPlayer SDK 实践:我们为什么把直播推播链路拆成一套可复用能力
设计模式·harmonyos·直播
一维Ace5 天前
HarmonyOS ArkTS 按钮组件全解:Button、Toggle 状态交互实战
harmonyos