【HarmonyOS实战】OpenHarmony + RN:自定义 useForm 表单管理

OpenHarmony + RN:自定义 useForm 表单管理(改写版)


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

  • [OpenHarmony + RN:自定义 useForm 表单管理(改写版)](#OpenHarmony + RN:自定义 useForm 表单管理(改写版))
    • 摘要
    • 目录
    • [1. 表单管理需求分析](#1. 表单管理需求分析)
      • [1.1 传统表单管理痛点](#1.1 传统表单管理痛点)
      • [1.2 OpenHarmony 平台特殊因素](#1.2 OpenHarmony 平台特殊因素)
    • [2. TypeScript 类型系统设计](#2. TypeScript 类型系统设计)
    • [3. 核心 useForm Hook 实现](#3. 核心 useForm Hook 实现)
    • [4. 表单验证器实现](#4. 表单验证器实现)
    • [5. OpenHarmony 平台适配](#5. OpenHarmony 平台适配)
    • [6. 键盘管理工具](#6. 键盘管理工具)
    • [7. 完整使用示例](#7. 完整使用示例)
      • [7.1 登录表单组件](#7.1 登录表单组件)
      • [7.2 注册表单组件(带密码确认)](#7.2 注册表单组件(带密码确认))
    • [8. 最佳实践总结](#8. 最佳实践总结)
      • [8.1 表单管理最佳实践](#8.1 表单管理最佳实践)
      • [8.2 OpenHarmony 适配检查清单](#8.2 OpenHarmony 适配检查清单)
      • [8.3 性能优化建议](#8.3 性能优化建议)
    • 参考资料

摘要

本文深入探讨在 OpenHarmony 6.0.0 平台上使用 React Native 实现自定义表单管理 Hook (useForm) 的完整方案。通过 useForm 的设计,我们解决了表单状态管理、验证规则和提交处理三大核心问题,并针对 OpenHarmony 6.0.0 平台的特殊需求进行了优化适配。


欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

目录

  1. 表单管理需求分析
  2. [TypeScript 类型系统设计](#TypeScript 类型系统设计)
  3. [核心 useForm Hook 实现](#核心 useForm Hook 实现)
  4. 表单验证器实现
  5. [OpenHarmony 平台适配](#OpenHarmony 平台适配)
  6. 键盘管理工具
  7. 完整使用示例
  8. 最佳实践总结

1. 表单管理需求分析

1.1 传统表单管理痛点

痛点 描述 影响
状态分散 表单字段状态分散在组件各处 代码维护困难
验证冗余 需要为每个字段单独编写验证逻辑 代码重复率高
提交耦合 提交逻辑与 UI 组件紧密耦合 难以复用
跨平台差异 不同平台输入行为不一致 兼容性问题

1.2 OpenHarmony 平台特殊因素

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                  OpenHarmony 表单特殊考量                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────┐ │
│  │  输入组件行为   │    │  性能优化需求   │    │ 异步提交处理 │ │
│  │                 │    │                 │    │             │ │
│  │ • 焦点管理差异  │    │ • 频繁状态更新  │    │ • 网络模型   │ │
│  │ • 键盘行为不同  │    │ • 渲染性能敏感  │    │ • 超时处理   │ │
│  │ • 输入法处理    │    │ • 批量更新需求  │    │ • 重试机制   │ │
│  └─────────────────┘    └─────────────────┘    └─────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. TypeScript 类型系统设计

typescript 复制代码
// ============================================
// types/form.ts
// 表单管理类型定义
// ============================================

/**
 * 验证规则接口
 */
export interface ValidationRule<T = any> {
  /** 必填验证 */
  required?: boolean;
  /** 最小长度 */
  minLength?: number;
  /** 最大长度 */
  maxLength?: number;
  /** 最小值 */
  min?: number;
  /** 最大值 */
  max?: number;
  /** 正则表达式验证 */
  pattern?: RegExp;
  /** 自定义同步验证函数 */
  validate?: (value: T, formData?: Record<string, any>) => string | null | boolean;
  /** 自定义错误消息 */
  message?: string;
}

/**
 * 字段规则配置(支持多规则)
 */
export type FieldRules<T = any> = ValidationRule<T> | ValidationRule<T>[];

/**
 * 表单验证规则映射
 */
export type FormValidationRules<T extends Record<string, any>> = {
  [K in keyof T]?: FieldRules<T[K]>;
};

/**
 * 表单状态接口
 */
export interface FormState<T extends Record<string, any> = Record<string, any>> {
  /** 表单数据 */
  values: T;
  /** 错误信息 */
  errors: Partial<Record<keyof T, string | null>>;
  /** 触碰状态(是否被聚焦过) */
  touched: Partial<Record<keyof T, boolean>>;
  /** 是否已被修改(脏数据) */
  isDirty: boolean;
  /** 是否验证通过 */
  isValid: boolean;
  /** 是否正在提交 */
  isSubmitting: boolean;
  /** 提交次数 */
  submitCount: number;
}

/**
 * 表单操作接口
 */
export interface FormActions<T extends Record<string, any> = Record<string, any>> {
  /** 设置字段值 */
  setValue: <K extends keyof T>(field: K, value: T[K]) => void;
  /** 批量设置字段值 */
  setValues: (values: Partial<T>) => void;
  /** 设置错误信息 */
  setError: <K extends keyof T>(field: K, error: string | null) => void;
  /** 批量设置错误信息 */
  setErrors: (errors: Partial<Record<keyof T, string | null>>) => void;
  /** 设置触碰状态 */
  setTouched: <K extends keyof T>(field: K, touched: boolean) => void;
  /** 设置提交状态 */
  setSubmitting: (isSubmitting: boolean) => void;
  /** 验证单个字段 */
  validateField: <K extends keyof T>(field: K) => Promise<boolean>;
  /** 验证所有字段 */
  validate: () => Promise<boolean>;
  /** 重置表单 */
  reset: (values?: Partial<T>) => void;
  /** 清除所有错误 */
  clearErrors: () => void;
}

/**
 * 表单返回值接口
 */
export interface UseFormReturn<T extends Record<string, any> = Record<string, any>>
  extends FormState<T>,
    FormActions<T> {
  /** 字段变化处理 */
  handleChange: <K extends keyof T>(field: K) => (value: T[K]) => void;
  /** 字段失焦处理 */
  handleBlur: <K extends keyof T>(field: K) => () => void;
  /** 提交处理 */
  handleSubmit: (
    onSubmit: (values: T, actions: FormActions<T>) => void | Promise<void>
  ) => (e?: any) => Promise<void>;
}

/**
 * useForm 配置选项
 */
export interface UseFormOptions<T extends Record<string, any>> {
  /** 初始值 */
  initialValues: T;
  /** 验证规则 */
  validationRules?: FormValidationRules<T>;
  /** 验证触发时机 */
  mode?: 'onBlur' | 'onChange' | 'onChangeBlur' | 'all';
  /** 验证防抖延迟(毫秒) */
  debounceDelay?: number;
  /** 提交前是否验证 */
  validateOnSubmit?: boolean;
  /** 重置时是否重置到初始值 */
  resetToInitial?: boolean;
}

/**
 * 验证结果类型
 */
export type ValidationResult = string | null | boolean;

/**
 * 字段验证器类型
 */
export type FieldValidator<T = any> = (
  value: T,
  formData?: Record<string, any>
) => ValidationResult | Promise<ValidationResult>;

3. 核心 useForm Hook 实现

typescript 复制代码
// ============================================
// hooks/useForm.ts
// 表单管理 Hook
// ============================================

import { useState, useCallback, useRef, useEffect } from 'react';
import type {
  UseFormOptions,
  UseFormReturn,
  FormState,
  FormActions,
  ValidationRule,
  FieldValidator,
  ValidationResult
} from '../types/form';
import { createFieldValidator } from '../utils/validators';

/**
 * 表单管理 Hook
 * @param options 表单配置选项
 * @returns 表单状态和方法
 */
export function useForm<T extends Record<string, any>>(
  options: UseFormOptions<T>
): UseFormReturn<T> {
  const {
    initialValues,
    validationRules = {},
    mode = 'onBlur',
    debounceDelay = 300,
    validateOnSubmit = true,
    resetToInitial = true
  } = options;

  // 保存初始值的引用
  const initialValuesRef = useRef(initialValues);
  const debounceTimersRef = useRef<Map<keyof T, ReturnType<typeof setTimeout>>>(new Map());
  const submittingRef = useRef(false);

  // 表单状态
  const [state, setState] = useState<FormState<T>>({
    values: { ...initialValues },
    errors: {},
    touched: {},
    isDirty: false,
    isValid: true,
    isSubmitting: false,
    submitCount: 0
  });

  /**
   * 验证单个字段
   */
  const validateField = useCallback(
    async <K extends keyof T>(field: K): Promise<boolean> => {
      const rules = validationRules[field];
      const value = state.values[field];

      if (!rules) {
        // 无规则则清除错误
        setState(prev => ({
          ...prev,
          errors: { ...prev.errors, [field]: null }
        }));
        return true;
      }

      // 执行验证
      const result = await executeValidation(value, rules, state.values);
      const isValid = result === null || result === true;
      const error = !isValid ? (typeof result === 'string' ? result : null) : null;

      setState(prev => ({
        ...prev,
        errors: { ...prev.errors, [field]: error },
        isValid: Object.keys(prev.errors).reduce((acc, key) => {
          if (key === String(field)) return acc && isValid;
          return acc && prev.errors[key as keyof T] === null;
        }, true)
      }));

      return isValid;
    },
    [state.values, validationRules]
  );

  /**
   * 验证所有字段
   */
  const validate = useCallback(async (): Promise<boolean> => {
    const fieldNames = Object.keys(validationRules) as Array<keyof T>;
    const results = await Promise.all(
      fieldNames.map(field => validateField(field))
    );
    return results.every(r => r);
  }, [validationRules, validateField]);

  /**
   * 设置字段值
   */
  const setValue = useCallback(
    <K extends keyof T>(field: K, value: T[K]) => {
      setState(prev => {
        const newValues = { ...prev.values, [field]: value };
        const isDirty =
          JSON.stringify(newValues) !== JSON.stringify(initialValuesRef.current);

        return {
          ...prev,
          values: newValues,
          isDirty
        };
      });

      // 根据模式决定是否验证
      if (mode === 'onChange' || mode === 'onChangeBlur' || mode === 'all') {
        // 防抖验证
        const timer = debounceTimersRef.current.get(field);
        if (timer) clearTimeout(timer);

        debounceTimersRef.current.set(
          field,
          setTimeout(() => {
            validateField(field);
            debounceTimersRef.current.delete(field);
          }, debounceDelay)
        );
      }
    },
    [mode, debounceDelay, validateField]
  );

  /**
   * 批量设置字段值
   */
  const setValues = useCallback((values: Partial<T>) => {
    setState(prev => {
      const newValues = { ...prev.values, ...values };
      const isDirty =
        JSON.stringify(newValues) !== JSON.stringify(initialValuesRef.current);

      return {
        ...prev,
        values: newValues,
        isDirty
      };
    });
  }, []);

  /**
   * 设置错误信息
   */
  const setError = useCallback(<K extends keyof T>(field: K, error: string | null) => {
    setState(prev => ({
      ...prev,
      errors: { ...prev.errors, [field]: error }
    }));
  }, []);

  /**
   * 批量设置错误信息
   */
  const setErrors = useCallback((errors: Partial<Record<keyof T, string | null>>) => {
    setState(prev => ({
      ...prev,
      errors: { ...prev.errors, ...errors }
    }));
  }, []);

  /**
   * 设置触碰状态
   */
  const setTouched = useCallback(<K extends keyof T>(field: K, touched: boolean) => {
    setState(prev => ({
      ...prev,
      touched: { ...prev.touched, [field]: touched }
    }));

    // 根据模式决定是否验证
    if (mode === 'onBlur' || mode === 'onChangeBlur' || mode === 'all') {
      validateField(field);
    }
  }, [mode, validateField]);

  /**
   * 设置提交状态
   */
  const setSubmitting = useCallback((isSubmitting: boolean) => {
    submittingRef.current = isSubmitting;
    setState(prev => ({
      ...prev,
      isSubmitting
    }));
  }, []);

  /**
   * 重置表单
   */
  const reset = useCallback((values?: Partial<T>) => {
    // 清除所有定时器
    debounceTimersRef.current.forEach(timer => clearTimeout(timer));
    debounceTimersRef.current.clear();

    const resetValues = values
      ? { ...initialValuesRef.current, ...values }
      : resetToInitial
      ? { ...initialValuesRef.current }
      : state.values;

    setState({
      values: resetValues,
      errors: {},
      touched: {},
      isDirty: false,
      isValid: true,
      isSubmitting: false,
      submitCount: 0
    });
  }, [resetToInitial, state.values]);

  /**
   * 清除所有错误
   */
  const clearErrors = useCallback(() => {
    setState(prev => ({
      ...prev,
      errors: {},
      isValid: true
    }));
  }, []);

  /**
   * 字段变化处理
   */
  const handleChange = useCallback(
    <K extends keyof T>(field: K) => (value: T[K]) => {
      setValue(field, value);
    },
    [setValue]
  );

  /**
   * 字段失焦处理
   */
  const handleBlur = useCallback(
    <K extends keyof T>(field: K) => () => {
      setTouched(field, true);
    },
    [setTouched]
  );

  /**
   * 提交处理
   */
  const handleSubmit = useCallback(
    (
      onSubmit: (values: T, actions: FormActions<T>) => void | Promise<void>
    ) => async (e?: any) => {
      e?.preventDefault();

      // 验证表单
      const isValid = validateOnSubmit ? await validate() : true;

      if (!isValid) {
        setState(prev => ({
          ...prev,
          submitCount: prev.submitCount + 1
        }));
        return;
      }

      // 执行提交
      setSubmitting(true);
      setState(prev => ({
        ...prev,
        submitCount: prev.submitCount + 1
      }));

      try {
        const actions: FormActions<T> = {
          setValue,
          setValues,
          setError,
          setErrors,
          setTouched,
          setSubmitting,
          validateField,
          validate,
          reset,
          clearErrors
        };

        await onSubmit(state.values, actions);
      } catch (error) {
        console.error('Form submission error:', error);
        throw error;
      } finally {
        setSubmitting(false);
      }
    },
    [validate, validateOnSubmit, state.values, setSubmitting, setValue, setValues, setError, setErrors, setTouched, validateField, reset, clearErrors]
  );

  // 组件卸载时清理定时器
  useEffect(() => {
    return () => {
      debounceTimersRef.current.forEach(timer => clearTimeout(timer));
      debounceTimersRef.current.clear();
    };
  }, []);

  return {
    ...state,
    setValue,
    setValues,
    setError,
    setErrors,
    setTouched,
    setSubmitting,
    validateField,
    validate,
    reset,
    clearErrors,
    handleChange,
    handleBlur,
    handleSubmit
  };
}

/**
 * 执行验证规则
 */
async function executeValidation<T>(
  value: T,
  rules: ValidationRule<T> | ValidationRule<T>[],
  formData: Record<string, any>
): Promise<ValidationResult> {
  const rulesArray = Array.isArray(rules) ? rules : [rules];

  for (const rule of rulesArray) {
    // 必填验证
    if (rule.required !== undefined && rule.required) {
      const requiredResult = validateRequired(value);
      if (requiredResult !== null) {
        return rule.message || requiredResult;
      }
    }

    // 如果值为空且非必填,跳过其他验证
    if (isEmpty(value)) {
      return null;
    }

    // 最小长度验证
    if (rule.minLength !== undefined) {
      const result = validateMinLength(value, rule.minLength);
      if (result !== null) return rule.message || result;
    }

    // 最大长度验证
    if (rule.maxLength !== undefined) {
      const result = validateMaxLength(value, rule.maxLength);
      if (result !== null) return rule.message || result;
    }

    // 最小值验证
    if (rule.min !== undefined) {
      const result = validateMin(value, rule.min);
      if (result !== null) return rule.message || result;
    }

    // 最大值验证
    if (rule.max !== undefined) {
      const result = validateMax(value, rule.max);
      if (result !== null) return rule.message || result;
    }

    // 正则表达式验证
    if (rule.pattern) {
      const result = validatePattern(value, rule.pattern);
      if (result !== null) return rule.message || result;
    }

    // 自定义验证
    if (rule.validate) {
      const result = await rule.validate(value, formData);
      if (result !== true && result !== null) {
        return typeof result === 'string' ? result : rule.message || '验证失败';
      }
    }
  }

  return null;
}

/**
 * 验证器函数集合
 */
function validateRequired<T>(value: T): ValidationResult {
  if (value === null || value === undefined) return '此字段为必填项';
  if (typeof value === 'string' && value.trim() === '') return '此字段为必填项';
  if (Array.isArray(value) && value.length === 0) return '至少选择一项';
  return null;
}

function isEmpty<T>(value: T): boolean {
  if (value === null || value === undefined) return true;
  if (typeof value === 'string' && value.trim() === '') return true;
  if (Array.isArray(value) && value.length === 0) return true;
  return false;
}

function validateMinLength<T>(value: T, min: number): ValidationResult {
  const length = getValueLength(value);
  return length < min ? `长度不能少于 ${min} 个字符` : null;
}

function validateMaxLength<T>(value: T, max: number): ValidationResult {
  const length = getValueLength(value);
  return length > max ? `长度不能超过 ${max} 个字符` : null;
}

function validateMin<T>(value: T, min: number): ValidationResult {
  if (typeof value === 'number') return value < min ? `不能小于 ${min}` : null;
  if (typeof value === 'string') {
    const num = parseFloat(value);
    return isNaN(num) || num < min ? `不能小于 ${min}` : null;
  }
  return null;
}

function validateMax<T>(value: T, max: number): ValidationResult {
  if (typeof value === 'number') return value > max ? `不能大于 ${max}` : null;
  if (typeof value === 'string') {
    const num = parseFloat(value);
    return isNaN(num) || num > max ? `不能大于 ${max}` : null;
  }
  return null;
}

function validatePattern<T>(value: T, pattern: RegExp): ValidationResult {
  const strValue = String(value);
  return !pattern.test(strValue) ? '格式不正确' : null;
}

function getValueLength<T>(value: T): number {
  if (value === null || value === undefined) return 0;
  if (typeof value === 'string' || Array.isArray(value)) return value.length;
  if (typeof value === 'number') return String(value).length;
  return String(value).length;
}

/**
 * 创建字段验证器
 */
export function createFieldValidator<T>(
  rules: ValidationRule<T> | ValidationRule<T>[]
): FieldValidator<T> {
  return (value: T, formData?: Record<string, any>) => {
    return executeValidation(value, rules, formData || {});
  };
}

/**
 * 常用正则表达式预设
 */
export const ValidationPatterns = {
  /** 电子邮箱 */
  email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  /** 手机号(中国大陆) */
  phone: /^1[3-9]\d{9}$/,
  /** 身份证号(中国大陆) */
  idCard: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/,
  /** URL */
  url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
  /** 用户名 */
  username: /^[a-zA-Z0-9_]{3,20}$/,
  /** 密码(强) */
  passwordStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
  /** 密码(基础) */
  passwordBasic: /^.{6,}$/,
  /** 数字 */
  number: /^-?\d+(\.\d+)?$/,
  /** 正整数 */
  positiveInteger: /^[1-9]\d*$/,
} as const;

4. 表单验证器实现

typescript 复制代码
// ============================================
// utils/validators.ts
// 表单验证工具
// ============================================

import type { ValidationRule, ValidationResult } from '../types/form';

/**
 * 验证器类
 * 提供静态方法执行各种验证
 */
export class Validator {
  /**
   * 必填验证
   */
  static required(message?: string): ValidationRule {
    return {
      required: true,
      message: message || '此字段为必填项'
    };
  }

  /**
   * 最小长度验证
   */
  static minLength(min: number, message?: string): ValidationRule {
    return {
      minLength: min,
      message: message || `长度不能少于 ${min} 个字符`
    };
  }

  /**
   * 最大长度验证
   */
  static maxLength(max: number, message?: string): ValidationRule {
    return {
      maxLength: max,
      message: message || `长度不能超过 ${max} 个字符`
    };
  }

  /**
   * 范围长度验证
   */
  static length(min: number, max: number, message?: string): ValidationRule {
    return {
      minLength: min,
      maxLength: max,
      message: message || `长度必须在 ${min} 到 ${max} 个字符之间`
    };
  }

  /**
   * 最小值验证
   */
  static min(min: number, message?: string): ValidationRule {
    return {
      min,
      message: message || `不能小于 ${min}`
    };
  }

  /**
   * 最大值验证
   */
  static max(max: number, message?: string): ValidationRule {
    return {
      max,
      message: message || `不能大于 ${max}`
    };
  }

  /**
   * 数值范围验证
   */
  static range(min: number, max: number, message?: string): ValidationRule {
    return {
      min,
      max,
      message: message || `必须在 ${min} 到 ${max} 之间`
    };
  }

  /**
   * 正则表达式验证
   */
  static pattern(regex: RegExp, message?: string): ValidationRule {
    return {
      pattern: regex,
      message: message || '格式不正确'
    };
  }

  /**
   * 电子邮箱验证
   */
  static email(message?: string): ValidationRule {
    return this.pattern(
      /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
      message || '请输入有效的邮箱地址'
    );
  }

  /**
   * 手机号验证
   */
  static phone(message?: string): ValidationRule {
    return this.pattern(
      /^1[3-9]\d{9}$/,
      message || '请输入有效的手机号码'
    );
  }

  /**
   * URL 验证
   */
  static url(message?: string): ValidationRule {
    return this.pattern(
      /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
      message || '请输入有效的 URL'
    );
  }

  /**
   * 自定义验证函数
   */
  static custom<T = any>(
    validator: (value: T, formData?: Record<string, any>) => ValidationResult,
    message?: string
  ): ValidationRule<T> {
    return {
      validate: validator,
      message
    };
  }

  /**
   * 组合多个验证规则
   */
  static combine<T = any>(...rules: ValidationRule<T>[]): ValidationRule<T>[] {
    return rules;
  }

  /**
   * 条件验证
   */
  static when<T = any>(
    condition: (value: T, formData?: Record<string, any>) => boolean,
    rules: ValidationRule | ValidationRule[]
  ): ValidationRule<T> {
    return {
      validate: (value, formData) => {
        if (!condition(value, formData)) return true;
        // 这里简化处理,实际应该递归执行规则
        return true;
      }
    };
  }
}

/**
 * 创建异步验证器
 */
export function createAsyncValidator<T>(
  asyncValidator: (value: T) => Promise<ValidationResult>,
  delay: number = 500
): ValidationRule<T> {
  return {
    validate: async (value: T) => {
      // 模拟网络延迟
      await new Promise(resolve => setTimeout(resolve, delay));
      return asyncValidator(value);
    }
  };
}

/**
 * 常用验证器预设
 */
export const Validators = {
  /** 必填 */
  required: (message?: string) => Validator.required(message),
  /** 邮箱 */
  email: (message?: string) => Validator.email(message),
  /** 手机号 */
  phone: (message?: string) => Validator.phone(message),
  /** URL */
  url: (message?: string) => Validator.url(message),
  /** 用户名 */
  username: (message?: string) =>
    Validator.pattern(/^[a-zA-Z0-9_]{3,20}$/, message || '用户名必须是3-20位字母、数字或下划线'),
  /** 密码(基础) */
  password: (message?: string) =>
    Validator.pattern(/^.{6,}$/, message || '密码至少6位'),
  /** 密码(强) */
  passwordStrong: (message?: string) =>
    Validator.pattern(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
      message || '密码必须包含大小写字母和数字,至少8位'
    ),
  /** 确认密码 */
  confirmPassword: (passwordFieldName: string, message?: string): ValidationRule => ({
    validate: (value: string, formData) =>
      value === formData[passwordFieldName] || message || '两次输入的密码不一致'
  })
} as const;

/**
 * 跨字段验证器
 */
export class CrossFieldValidator {
  /**
   * 字段匹配验证(如确认密码)
   */
  static match(
    targetField: string,
    message?: string
  ): ValidationRule {
    return {
      validate: (value: any, formData?: Record<string, any>) => {
        const targetValue = formData?.[targetField];
        return value === targetValue || message || `必须与 ${targetField} 一致`;
      }
    };
  }

  /**
   * 字段范围验证(如开始日期必须早于结束日期)
   */
  static before(
    targetField: string,
    message?: string
  ): ValidationRule<Date> {
    return {
      validate: (value: Date, formData?: Record<string, any>) => {
        const targetValue = formData?.[targetField];
        if (!value || !targetValue) return true;
        return value <= targetValue || message || `必须在 ${targetField} 之前`;
      }
    };
  }

  /**
   * 字段依赖验证(如当选择了A时,B必填)
   */
  static requiredWhen(
    condition: (formData: Record<string, any>) => boolean,
    message?: string
  ): ValidationRule {
    return {
      validate: (value: any, formData?: Record<string, any>) => {
        if (!condition(formData || {})) return true;
        return Validator.required().validate?.(value) || message || '此字段为必填项';
      }
    };
  }
}

5. OpenHarmony 平台适配

typescript 复制代码
// ============================================
// platform/OpenHarmonyFormAdapter.ts
// OpenHarmony 表单平台适配
// ============================================

import { Platform, Keyboard } from 'react-native';
import type { UseFormOptions } from '../types/form';

/**
 * 平台类型
 */
export enum PlatformType {
  ANDROID = 'android',
  IOS = 'ios',
  OPENHARMONY = 'openharmony',
  WEB = 'web'
}

/**
 * 获取当前平台
 */
export function getPlatform(): PlatformType {
  const platform = Platform.OS;
  if (platform === 'harmony' || platform === 'ohos') {
    return PlatformType.OPENHARMONY;
  }
  return platform as PlatformType.ANDROID | PlatformType.IOS | PlatformType.WEB;
}

/**
 * 是否为 OpenHarmony 平台
 */
export function isOpenHarmony(): boolean {
  return getPlatform() === PlatformType.OPENHARMONY;
}

/**
 * OpenHarmony 优化的 useForm 配置
 */
export function getOpenHarmonyFormOptions<T>(
  baseOptions: UseFormOptions<T>
): UseFormOptions<T> {
  if (!isOpenHarmony()) {
    return baseOptions;
  }

  // OpenHarmony 平台优化配置
  return {
    ...baseOptions,
    // 使用更长的防抖延迟,因为 OpenHarmony 输入法响应较慢
    debounceDelay: baseOptions.debounceDelay ?? 500,
    // 推荐使用 onBlur 模式,减少频繁验证
    mode: baseOptions.mode ?? 'onBlur'
  };
}

/**
 * OpenHarmony 键盘行为差异对照表
 */
export const KeyboardBehaviorDifferences = {
  android: {
    dismissOnOutsidePress: true,
    animation: 'smooth',
    autoAdjust: true
  },
  ios: {
    dismissOnOutsidePress: true,
    animation: 'smooth',
    autoAdjust: true
  },
  openharmony: {
    dismissOnOutsidePress: false, // 需要手动处理
    animation: 'instant',
    autoAdjust: false // 需要手动处理布局调整
  }
} as const;

/**
 * OpenHarmony 焦点管理工具
 */
export class FocusManager {
  private static activeInputRef: any = null;

  /**
   * 注册当前活动输入框
   */
  static registerInput(ref: any) {
    this.activeInputRef = ref;
  }

  /**
   * 取消当前输入框焦点
   */
  static dismissActiveInput() {
    if (isOpenHarmony()) {
      // OpenHarmony 需要显式调用 blur
      this.activeInputRef?.blur?.();
    }
    Keyboard.dismiss();
    this.activeInputRef = null;
  }

  /**
   * 创建外部点击处理器
   */
  static createOutsidePressHandler() {
    if (!isOpenHarmony()) {
      return undefined; // Android/iOS 不需要
    }

    return () => {
      this.dismissActiveInput();
    };
  }
}

/**
 * 表单容器组件属性
 */
export interface FormContainerProps {
  children: React.ReactNode;
  style?: any;
  contentContainerStyle?: any;
}

/**
 * OpenHarmony 优化的表单容器
 */
export function FormContainer({
  children,
  style,
  contentContainerStyle
}: FormContainerProps) {
  const outsidePressHandler = FocusManager.createOutsidePressHandler();

  return (
    <View
      style={style}
      onStartShouldSetResponder={() => !!outsidePressHandler}
      onResponderRelease={outsidePressHandler}
    >
      {children}
    </View>
  );
}

6. 键盘管理工具

typescript 复制代码
// ============================================
// utils/KeyboardManager.ts
// 键盘管理工具
// ============================================

import {
  Keyboard,
  Platform,
  KeyboardAvoidingView,
  ScrollView,
  View
} from 'react-native';
import { isOpenHarmony } from '../platform/OpenHarmonyFormAdapter';
import React, { useState, useEffect } from 'react';

/**
 * 键盘高度 Hook
 */
export function useKeyboardHeight() {
  const [keyboardHeight, setKeyboardHeight] = useState(0);

  useEffect(() => {
    const showSubscription = Keyboard.addListener('keyboardDidShow', (e) => {
      setKeyboardHeight(e.endCoordinates.height);
    });
    const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
      setKeyboardHeight(0);
    });

    return () => {
      showSubscription.remove();
      hideSubscription.remove();
    };
  }, []);

  return keyboardHeight;
}

/**
 * 键盘可见状态 Hook
 */
export function useKeyboardVisible() {
  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);

  useEffect(() => {
    const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
      setIsKeyboardVisible(true);
    });
    const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
      setIsKeyboardVisible(false);
    });

    return () => {
      showSubscription.remove();
      hideSubscription.remove();
    };
  }, []);

  return isKeyboardVisible;
}

/**
 * KeyboardAvoidingView 包装组件
 * OpenHarmony 平台使用特殊配置
 */
export function SmartKeyboardAvoidingView({
  children,
  style,
  contentContainerStyle
}: {
  children: React.ReactNode;
  style?: any;
  contentContainerStyle?: any;
}) {
  if (isOpenHarmony()) {
    // OpenHarmony 使用 padding 行为
    return (
      <KeyboardAvoidingView
        style={style}
        behavior="padding"
        keyboardVerticalOffset={0}
      >
        {children}
      </KeyboardAvoidingView>
    );
  }

  // iOS/Android 默认配置
  return (
    <KeyboardAvoidingView
      style={style}
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
    >
      {children}
    </KeyboardAvoidingView>
  );
}

/**
 * 带键盘避让的 ScrollView
 */
export function KeyboardAwareScrollView({
  children,
  style,
  contentContainerStyle
}: {
  children: React.ReactNode;
  style?: any;
  contentContainerStyle?: any;
}) {
  const keyboardHeight = useKeyboardHeight();

  return (
    <ScrollView
      style={style}
      contentContainerStyle={[
        contentContainerStyle,
        { paddingBottom: keyboardHeight }
      ]}
      keyboardShouldPersistTaps="handled"
    >
      {children}
    </ScrollView>
  );
}

/**
 * 输入框滚动视图管理器
 * 用于处理键盘遮挡输入框的问题
 */
export class InputScrollManager {
  private static scrollRef: any = null;
  private static inputPositionY: number = 0;

  /**
   * 注册滚动视图引用
   */
  static registerScroll(ref: any) {
    this.scrollRef = ref;
  }

  /**
   * 记录输入框位置
   */
  static recordInputPosition(y: number) {
    this.inputPositionY = y;
  }

  /**
   * 滚动到输入框位置
   */
  static scrollToInput(offset: number = 100) {
    if (!this.scrollRef) return;

    const scrollToY = Math.max(0, this.inputPositionY - offset);

    this.scrollRef.scrollTo({
      y: scrollToY,
      animated: true
    });
  }

  /**
   * 清理
   */
  static cleanup() {
    this.scrollRef = null;
    this.inputPositionY = 0;
  }
}

/**
 * 自动滚动输入框组件
 */
export function AutoScrollInput({
  children,
  scrollOffset = 100
}: {
  children: React.ReactElement;
  scrollOffset?: number;
}) {
  const isKeyboardVisible = useKeyboardVisible();

  useEffect(() => {
    if (isKeyboardVisible) {
      // 延迟滚动,等待键盘动画完成
      setTimeout(() => {
        InputScrollManager.scrollToInput(scrollOffset);
      }, isOpenHarmony() ? 100 : 300);
    }
  }, [isKeyboardVisible, scrollOffset]);

  return children;
}

7. 完整使用示例

7.1 登录表单组件

typescript 复制代码
// ============================================
// examples/LoginForm.tsx
// 登录表单示例
// ============================================

import React from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  ActivityIndicator,
  Alert
} from 'react-native';
import { useForm } from '../hooks/useForm';
import { Validators, CrossFieldValidator } from '../utils/validators';
import { FormContainer } from '../platform/OpenHarmonyFormAdapter';
import { SmartKeyboardAvoidingView } from '../utils/KeyboardManager';

// 登录表单数据类型
interface LoginFormData {
  username: string;
  password: string;
  rememberMe: boolean;
}

export const LoginForm: React.FC = () => {
  const form = useForm<LoginFormData>({
    initialValues: {
      username: '',
      password: '',
      rememberMe: false
    },
    validationRules: {
      username: Validator.combine(
        Validators.required('请输入用户名'),
        Validators.username(),
        Validator.minLength(3),
        Validator.maxLength(20)
      ),
      password: Validator.combine(
        Validators.required('请输入密码'),
        Validator.minLength(6),
        Validator.maxLength(20)
      )
    },
    mode: 'onBlur',
    debounceDelay: isOpenHarmony() ? 500 : 300
  });

  const handleSubmit = async (values: LoginFormData) => {
    try {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1500));
      Alert.alert('登录成功', `欢迎, ${values.username}!`);
    } catch (error) {
      Alert.alert('登录失败', '用户名或密码错误');
    }
  };

  return (
    <SmartKeyboardAvoidingView style={styles.container}>
      <FormContainer style={styles.content}>
        <ScrollView showsVerticalScrollIndicator={false}>
          {/* 标题 */}
          <Text style={styles.title}>欢迎回来</Text>
          <Text style={styles.subtitle}>登录以继续使用</Text>

          {/* 用户名输入 */}
          <View style={styles.inputGroup}>
            <Text style={styles.label}>用户名</Text>
            <TextInput
              style={[
                styles.input,
                form.errors.username && styles.inputError
              ]}
              value={form.values.username}
              onChangeText={form.handleChange('username')}
              onBlur={form.handleBlur('username')}
              placeholder="请输入用户名"
              placeholderTextColor="#999"
              autoCapitalize="none"
              autoCorrect={false}
            />
            {form.touched.username && form.errors.username && (
              <Text style={styles.error}>{form.errors.username}</Text>
            )}
          </View>

          {/* 密码输入 */}
          <View style={styles.inputGroup}>
            <Text style={styles.label}>密码</Text>
            <TextInput
              style={[
                styles.input,
                form.errors.password && styles.inputError
              ]}
              value={form.values.password}
              onChangeText={form.handleChange('password')}
              onBlur={form.handleBlur('password')}
              placeholder="请输入密码"
              placeholderTextColor="#999"
              secureTextEntry
              autoCapitalize="none"
              autoCorrect={false}
            />
            {form.touched.password && form.errors.password && (
              <Text style={styles.error}>{form.errors.password}</Text>
            )}
          </View>

          {/* 记住我 */}
          <TouchableOpacity
            style={styles.checkbox}
            onPress={() =>
              form.setValue('rememberMe', !form.values.rememberMe)
            }
          >
            <View
              style={[
                styles.checkboxBox,
                form.values.rememberMe && styles.checkboxChecked
              ]}
            />
            <Text style={styles.checkboxLabel}>记住我</Text>
          </TouchableOpacity>

          {/* 登录按钮 */}
          <TouchableOpacity
            style={[
              styles.submitButton,
              (!form.isValid || form.isSubmitting) && styles.submitButtonDisabled
            ]}
            onPress={form.handleSubmit(handleSubmit)}
            disabled={!form.isValid || form.isSubmitting}
          >
            {form.isSubmitting ? (
              <ActivityIndicator color="#fff" />
            ) : (
              <Text style={styles.submitButtonText}>登录</Text>
            )}
          </TouchableOpacity>

          {/* 表单状态 */}
          <View style={styles.formStatus}>
            <Text style={styles.statusText}>
              提交次数: {form.submitCount}
            </Text>
            <Text style={styles.statusText}>
              验证状态: {form.isValid ? '通过' : '失败'}
            </Text>
          </View>
        </ScrollView>
      </FormContainer>
    </SmartKeyboardAvoidingView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5'
  },
  content: {
    flex: 1,
    padding: 20
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 8
  },
  subtitle: {
    fontSize: 16,
    color: '#666',
    marginBottom: 32
  },
  inputGroup: {
    marginBottom: 20
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    color: '#333',
    marginBottom: 8
  },
  input: {
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingVertical: 14,
    fontSize: 16
  },
  inputError: {
    borderColor: '#ff3b30'
  },
  error: {
    color: '#ff3b30',
    fontSize: 12,
    marginTop: 6
  },
  checkbox: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 24
  },
  checkboxBox: {
    width: 20,
    height: 20,
    borderWidth: 2,
    borderColor: '#ddd',
    borderRadius: 4,
    marginRight: 8
  },
  checkboxChecked: {
    backgroundColor: '#34C759',
    borderColor: '#34C759'
  },
  checkboxLabel: {
    fontSize: 14,
    color: '#666'
  },
  submitButton: {
    backgroundColor: '#34C759',
    borderRadius: 12,
    paddingVertical: 16,
    alignItems: 'center'
  },
  submitButtonDisabled: {
    backgroundColor: '#ccc'
  },
  submitButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '700'
  },
  formStatus: {
    marginTop: 20,
    padding: 16,
    backgroundColor: '#f8f8f8',
    borderRadius: 8
  },
  statusText: {
    fontSize: 12,
    color: '#666',
    marginBottom: 4
  }
});

7.2 注册表单组件(带密码确认)

typescript 复制代码
// ============================================
// examples/RegistrationForm.tsx
// 注册表单示例
// ============================================

import React from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView
} from 'react-native';
import { useForm } from '../hooks/useForm';
import { Validators, CrossFieldValidator } from '../utils/validators';

interface RegistrationFormData {
  email: string;
  password: string;
  confirmPassword: string;
  agreeTerms: boolean;
}

export const RegistrationForm: React.FC = () => {
  const form = useForm<RegistrationFormData>({
    initialValues: {
      email: '',
      password: '',
      confirmPassword: '',
      agreeTerms: false
    },
    validationRules: {
      email: Validators.email(),
      password: Validator.combine(
        Validators.required('请输入密码'),
        Validators.passwordStrong()
      ),
      confirmPassword: CrossFieldValidator.match('password', '两次输入的密码不一致')
    },
    mode: 'onChangeBlur'
  });

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>创建账户</Text>

      {/* 邮箱 */}
      <View style={styles.field}>
        <Text style={styles.label}>邮箱</Text>
        <TextInput
          style={styles.input}
          value={form.values.email}
          onChangeText={form.handleChange('email')}
          onBlur={form.handleBlur('email')}
          placeholder="your@email.com"
          keyboardType="email-address"
          autoCapitalize="none"
        />
        {form.touched.email && form.errors.email && (
          <Text style={styles.error}>{form.errors.email}</Text>
        )}
      </View>

      {/* 密码 */}
      <View style={styles.field}>
        <Text style={styles.label}>密码</Text>
        <TextInput
          style={styles.input}
          value={form.values.password}
          onChangeText={form.handleChange('password')}
          onBlur={form.handleBlur('password')}
          placeholder="至少8位,包含大小写字母和数字"
          secureTextEntry
        />
        {form.touched.password && form.errors.password && (
          <Text style={styles.error}>{form.errors.password}</Text>
        )}
      </View>

      {/* 确认密码 */}
      <View style={styles.field}>
        <Text style={styles.label}>确认密码</Text>
        <TextInput
          style={styles.input}
          value={form.values.confirmPassword}
          onChangeText={form.handleChange('confirmPassword')}
          onBlur={form.handleBlur('confirmPassword')}
          placeholder="再次输入密码"
          secureTextEntry
        />
        {form.touched.confirmPassword && form.errors.confirmPassword && (
          <Text style={styles.error}>{form.errors.confirmPassword}</Text>
        )}
      </View>

      {/* 提交按钮 */}
      <TouchableOpacity
        style={[
          styles.submitButton,
          (!form.isValid || !form.values.agreeTerms) && styles.submitButtonDisabled
        ]}
        onPress={form.handleSubmit(async (values) => {
          // 提交逻辑
          console.log('注册:', values);
        })}
      >
        <Text style={styles.submitButtonText}>注册</Text>
      </TouchableOpacity>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#fff'
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 24
  },
  field: {
    marginBottom: 20
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    marginBottom: 8
  },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingVertical: 12
  },
  error: {
    color: '#ff3b30',
    fontSize: 12,
    marginTop: 4
  },
  submitButton: {
    backgroundColor: '#34C759',
    paddingVertical: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginTop: 8
  },
  submitButtonDisabled: {
    backgroundColor: '#ccc'
  },
  submitButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '700'
  }
});

8. 最佳实践总结

8.1 表单管理最佳实践

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      useForm 最佳实践架构                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌────────────────────┐      ┌────────────────────┐            │
│  │   数据层           │      │   验证层           │            │
│  │                    │      │                    │            │
│  │ • initialValues    │      │ • validationRules  │            │
│  │ • 状态管理         │      │ • 同步验证         │            │
│  │ • 脏数据检测       │      │ • 异步验证         │            │
│  └────────────────────┘      └────────────────────┘            │
│            │                          │                        │
│            └──────────┬───────────────┘                        │
│                       ▼                                        │
│            ┌────────────────────┐                              │
│            │   useForm Hook     │                              │
│            │                    │                              │
│            │ • 状态追踪         │                              │
│            │ • 防抖处理         │                              │
│            │ • 提交管理         │                              │
│            └────────────────────┘                              │
│                       │                                        │
│                       ▼                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │                    UI 组件层                           │    │
│  │                                                       │    │
│  │  • TextInput  • CheckBox  • Select  • DatePicker     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.2 OpenHarmony 适配检查清单

项目 检查点 说明
焦点管理 外部点击失焦 使用 FocusManager 处理
键盘处理 键盘避让 使用 SmartKeyboardAvoidingView
输入验证 防抖延迟 设置 500ms 防抖
滚动定位 输入框可见 使用 InputScrollManager
提交优化 超时重试 实现指数退避机制
状态持久化 暂停保存 保存表单到持久化存储

8.3 性能优化建议

  1. 状态更新优化

    • 使用 useCallback 缓存回调函数
    • 使用防抖减少频繁验证
    • 批量更新多个字段值
  2. 验证优化

    • 按需验证(onBlur 或 onChange)
    • 预编译正则表达式
    • 异步验证使用 Web Worker
  3. 渲染优化

    • 使用 React.memo 包装表单字段
    • 避免在渲染中创建对象/函数
    • 使用 useRef 存储非渲染数据

参考资料


📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
早點睡3906 小时前
高级进阶 ReactNative for Harmony 项目鸿蒙化三方库集成实战:react-native-video
react native·华为·harmonyos
开开心心就好6 小时前
发票合并打印工具,多页布局设置实时预览
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
前端不太难7 小时前
HarmonyOS 游戏项目,从 Demo 到可上线要跨过哪些坑
游戏·状态模式·harmonyos
全栈探索者7 小时前
列表渲染不用 map,用 ForEach!—— React 开发者的鸿蒙入门指南(第 4 期)
react.js·harmonyos·arkts·foreach·列表渲染
一只大侠的侠9 小时前
Flutter开源鸿蒙跨平台训练营 Day8获取轮播图网络数据并实现展示
flutter·开源·harmonyos
Lionel68910 小时前
鸿蒙Flutter跨平台开发:首页特惠推荐模块的实现
华为·harmonyos
盐焗西兰花10 小时前
鸿蒙学习实战之路-Reader Kit自定义页面背景最佳实践
学习·华为·harmonyos
果粒蹬i10 小时前
【HarmonyOS】DAY10:React Native开发应用品质升级:响应式布局与用户体验优化实践
华为·harmonyos·ux
早點睡39010 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-flash-message 消息提示三方库适配
react native·react.js·harmonyos