React Native开源鸿蒙跨平台训练营 Day18自定义useForm表单管理实战实现

OpenHarmony + RN 自定义useForm表单管理实战实现

在OpenHarmony 6.0.0平台结合React Native开发的过程中,表单管理是前端开发的核心场景之一,传统的表单处理方式存在状态分散、验证冗余等诸多问题,同时OpenHarmony平台的输入行为、性能要求等特殊因素也对表单管理提出了个性化需求。本文将详细讲解如何基于TypeScript打造自定义useForm Hook,实现高效、可复用、适配OpenHarmony平台的表单管理方案,涵盖类型设计、核心Hook实现、验证器开发、平台适配及实际应用示例等全流程内容。

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


一、表单管理需求分析

1.1 传统表单管理的核心痛点

传统RN开发中的表单管理方式,往往会将表单字段的状态分散在各个组件中,带来一系列开发和维护问题,具体痛点如下:

痛点 具体描述 实际影响
状态分散 表单各字段的value、校验状态等分散在组件各处,无统一管理 代码维护难度大,状态联动时易出现逻辑混乱
验证冗余 为每个表单字段单独编写验证逻辑,重复代码多 开发效率低,后期修改验证规则需多处调整
提交耦合 表单提交逻辑与UI组件紧密绑定,提交逻辑无法抽离 代码复用性差,不同表单无法共享提交逻辑
跨平台差异 不同平台的输入组件行为、键盘交互等存在不一致性 易出现跨平台兼容性问题,需单独做平台兼容

1.2 OpenHarmony平台的特殊考量

基于OpenHarmony 6.0.0的RN开发,除了传统表单的痛点外,还需要兼顾平台自身的特性,主要集中在输入组件行为、性能优化、异步提交处理三个方面:

  1. 输入组件行为差异:OpenHarmony平台的输入框焦点管理逻辑、键盘弹出/收起行为、输入法内容处理与Android/iOS存在区别,需针对性适配;
  2. 性能优化需求:OpenHarmony对组件频繁状态更新的渲染性能更为敏感,需要处理表单字段的批量更新,减少不必要的重渲染;
  3. 异步提交处理:OpenHarmony的网络请求模型、超时机制与其他平台不同,表单异步提交需实现专属的超时处理和重试机制。

二、TypeScript类型系统设计

为了保证useForm Hook的类型安全、参数校验和开发提示,基于TypeScript设计了一套完整的表单管理类型体系,覆盖验证规则、表单状态、操作方法、Hook配置等所有核心环节,所有类型统一维护在types/form.ts中。

核心类型定义

typescript 复制代码
/**
 * 单个验证规则接口
 */
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;                                             // 清除所有错误
}

/**
 * useForm Hook返回值(整合状态与操作方法)
 */
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 Hook配置选项
 */
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>;

三、核心useForm Hook实现

useForm是表单管理的核心,基于React的useStateuseCallbackuseRef等Hook实现,负责表单状态的统一管理、验证逻辑的执行、操作方法的封装,核心代码维护在hooks/useForm.ts中,整体设计遵循单一职责高内聚低耦合原则。

核心实现代码

typescript 复制代码
import { useState, useCallback, useRef, useEffect } from 'react';
import type {
  UseFormOptions,
  UseFormReturn,
  FormState,
  FormActions,
  ValidationRule,
  FieldValidator,
  ValidationResult
} from '../types/form';

/**
 * 表单管理核心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
  });

  /**
   * 执行单个验证规则
   */
  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) {
        if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0)) {
          return rule.message || '此字段为必填项';
        }
      }
      // 空值且非必填,跳过后续验证
      if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0)) {
        return null;
      }
      // 长度验证
      if (rule.minLength !== undefined) {
        const len = typeof value === 'string' || Array.isArray(value) ? value.length : String(value).length;
        if (len < rule.minLength) return rule.message || `长度不能少于${rule.minLength}个字符`;
      }
      if (rule.maxLength !== undefined) {
        const len = typeof value === 'string' || Array.isArray(value) ? value.length : String(value).length;
        if (len > rule.maxLength) return rule.message || `长度不能超过${rule.maxLength}个字符`;
      }
      // 数值范围验证
      if (rule.min !== undefined) {
        const num = typeof value === 'number' ? value : parseFloat(String(value));
        if (isNaN(num) || num < rule.min) return rule.message || `不能小于${rule.min}`;
      }
      if (rule.max !== undefined) {
        const num = typeof value === 'number' ? value : parseFloat(String(value));
        if (isNaN(num) || num > rule.max) return rule.message || `不能大于${rule.max}`;
      }
      // 正则验证
      if (rule.pattern) {
        if (!rule.pattern.test(String(value))) return rule.message || '格式不正确';
      }
      // 自定义验证函数
      if (rule.validate) {
        const result = await rule.validate(value, formData);
        if (result !== true && result !== null) {
          return typeof result === 'string' ? result : rule.message || '验证失败';
        }
      }
    }
    return null;
  }

  /**
   * 验证单个字段
   */
  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);
        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 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 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 }));
      // 封装操作方法并执行自定义提交逻辑
      const actions: FormActions<T> = {
        setValue, setValues, setError, setErrors, setTouched,
        setSubmitting, validateField, validate, reset, clearErrors
      };
      try {
        await onSubmit(state.values, actions);
      } catch (error) {
        console.error('表单提交失败:', error);
        throw error;
      } finally {
        setSubmitting(false);
      }
    },
    [validate, validateOnSubmit, state.values, setSubmitting, setValue, setValues, setError, setErrors, setTouched, validateField, reset, clearErrors]
  );

  /**
   * 重置表单(清除定时器+恢复状态)
   */
  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]);

  // 其他基础操作方法:setError、setErrors、setSubmitting、clearErrors、handleChange、handleBlur
  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 setSubmitting = useCallback((isSubmitting: boolean) => {
    submittingRef.current = isSubmitting;
    setState(prev => ({ ...prev, isSubmitting }));
  }, []);

  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]
  );

  // 组件卸载时清理防抖定时器
  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
  };
}

/**
 * 常用正则表达式预设
 */
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: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/, // URL
  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;

四、表单验证器实现

为了简化验证规则的编写,基于上述验证规则接口,封装了通用验证器工具类 ,提供必填、长度、数值范围、正则、邮箱、手机号等常用验证的快捷方法,同时支持自定义验证、跨字段验证、异步验证,代码维护在utils/validators.ts中,实现验证逻辑的复用

核心验证器代码

typescript 复制代码
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: async (value, formData) => {
        if (!condition(value, formData)) return true;
        const rulesArray = Array.isArray(rules) ? rules : [rules];
        for (const rule of rulesArray) {
          const result = await (rule.validate ? rule.validate(value, formData) : true);
          if (result !== true && result !== null) {
            return rule.message || '验证失败';
          }
        }
        return true;
      }
    };
  }
}

/**
 * 异步验证器创建方法
 * @param asyncValidator 异步验证函数
 * @param delay 延迟时间(默认500ms)
 * @returns 验证规则
 */
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: (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 requiredWhen(
    condition: (formData: Record<string, any>) => boolean,
    message?: string
  ): ValidationRule {
    return {
      validate: (value: any, formData?: Record<string, any>) => {
        if (!condition(formData || {})) return true;
        return (value !== null && value !== undefined && (typeof value !== 'string' || value.trim() !== '')) || message || '此字段为必填项';
      }
    };
  }
}

五、OpenHarmony平台适配

针对OpenHarmony平台的输入行为、键盘管理、性能特性等特殊点,开发了专属的平台适配工具 ,封装在platform/OpenHarmonyFormAdapter.ts中,实现一套代码多平台兼容,无需在业务代码中编写平台判断逻辑。

核心适配代码

typescript 复制代码
import { Platform, Keyboard, View } from 'react-native';
import type { UseFormOptions } from '../types/form';
import React from 'react';

/**
 * 平台类型枚举
 */
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配置优化
 * @param baseOptions 基础配置
 * @returns 适配后的配置
 */
export function getOpenHarmonyFormOptions<T>(
  baseOptions: UseFormOptions<T>
): UseFormOptions<T> {
  if (!isOpenHarmony()) return baseOptions;
  // OpenHarmony输入法响应较慢,增大防抖延迟;推荐onBlur验证,减少频繁渲染
  return {
    ...baseOptions,
    debounceDelay: baseOptions.debounceDelay ?? 500,
    mode: baseOptions.mode ?? 'onBlur'
  };
}

/**
 * 各平台键盘行为差异对照表
 */
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()) {
      this.activeInputRef?.blur?.(); // OpenHarmony需显式调用blur
    }
    Keyboard.dismiss();
    this.activeInputRef = null;
  }

  // 创建外部点击处理函数(OpenHarmony专属)
  static createOutsidePressHandler() {
    if (!isOpenHarmony()) return undefined;
    return () => this.dismissActiveInput();
  }
}

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

/**
 * OpenHarmony优化的表单容器(处理外部点击失焦)
 */
export function FormContainer({
  children,
  style,
  contentContainerStyle
}: FormContainerProps) {
  const outsidePressHandler = FocusManager.createOutsidePressHandler();
  // OpenHarmony通过手势响应实现外部点击失焦
  return (
    <View
      style={style}
      onStartShouldSetResponder={() => !!outsidePressHandler}
      onResponderRelease={outsidePressHandler}
    >
      {children}
    </View>
  );
}

六、键盘管理工具

OpenHarmony平台的键盘自动避让、滚动定位等行为与Android/iOS不一致,为此封装了键盘管理工具 ,提供键盘高度监听、智能避让、输入框自动滚动等能力,代码维护在utils/KeyboardManager.ts中,解决键盘遮挡输入框、布局错乱等问题。

核心键盘管理代码

typescript 复制代码
import { Keyboard, Platform, KeyboardAvoidingView, ScrollView } from 'react-native';
import { isOpenHarmony } from '../platform/OpenHarmonyFormAdapter';
import React, { useState, useEffect, ReactElement } from 'react';

/**
 * Hook:监听键盘高度变化
 * @returns 当前键盘高度
 */
export function useKeyboardHeight() {
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  useEffect(() => {
    const show = Keyboard.addListener('keyboardDidShow', (e) => setKeyboardHeight(e.endCoordinates.height));
    const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardHeight(0));
    return () => { show.remove(); hide.remove(); };
  }, []);
  return keyboardHeight;
}

/**
 * Hook:监听键盘可见状态
 * @returns 键盘是否可见
 */
export function useKeyboardVisible() {
  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
  useEffect(() => {
    const show = Keyboard.addListener('keyboardDidShow', () => setIsKeyboardVisible(true));
    const hide = Keyboard.addListener('keyboardDidHide', () => setIsKeyboardVisible(false));
    return () => { show.remove(); hide.remove(); };
  }, []);
  return isKeyboardVisible;
}

/**
 * 智能键盘避让组件(多平台适配)
 */
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使用padding,Android默认不处理
  return (
    <KeyboardAvoidingView style={style} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
      {children}
    </KeyboardAvoidingView>
  );
}

/**
 * 带键盘避让的滚动视图(自动添加底部内边距)
 */
export function KeyboardAwareScrollView({
  children,
  style,
  contentContainerStyle
}: {
  children: React.ReactNode;
  style?: any;
  contentContainerStyle?: any;
}) {
  const keyboardHeight = useKeyboardHeight();
  return (
    <ScrollView
      style={style}
      contentContainerStyle={[{ paddingBottom: keyboardHeight }, contentContainerStyle]}
      keyboardShouldPersistTaps="handled"
    >
      {children}
    </ScrollView>
  );
}

/**
 * 输入框滚动管理器(解决OpenHarmony键盘遮挡输入框)
 */
export class InputScrollManager {
  private static scrollRef: any = null;
  private static inputPositionY: number = 0;

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

  // 记录输入框Y轴位置
  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: ReactElement;
  scrollOffset?: number;
}) {
  const isKeyboardVisible = useKeyboardVisible();
  useEffect(() => {
    if (isKeyboardVisible) {
      // OpenHarmony键盘动画更快,缩短延迟
      setTimeout(() => InputScrollManager.scrollToInput(scrollOffset), isOpenHarmony() ? 100 : 300);
    }
  }, [isKeyboardVisible, scrollOffset]);
  return children;
}

七、完整使用示例

基于上述实现的useForm Hook、验证器、平台适配工具,提供两个典型的表单示例:登录表单 (基础表单+复选框)和注册表单(跨字段验证+密码确认),直接可在OpenHarmony 6.0.0 + RN环境中运行。

7.1 登录表单组件

tsx 复制代码
// examples/LoginForm.tsx
import React from 'react';
import {
  View, Text, TextInput, TouchableOpacity, StyleSheet,
  ActivityIndicator, Alert
} from 'react-native';
import { useForm } from '../hooks/useForm';
import { Validator, Validators } from '../utils/validators';
import { FormContainer, isOpenHarmony } from '../platform/OpenHarmonyFormAdapter';
import { SmartKeyboardAvoidingView, KeyboardAwareScrollView } from '../utils/KeyboardManager';

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

export const LoginForm: React.FC = () => {
  // 初始化表单,使用OpenHarmony优化配置
  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)
      )
    },
    debounceDelay: isOpenHarmony() ? 500 : 300,
    mode: 'onBlur'
  });

  // 表单提交逻辑
  const onLogin = async (values: LoginFormData) => {
    try {
      // 模拟OpenHarmony平台网络请求
      await new Promise(resolve => setTimeout(resolve, 1500));
      Alert.alert('登录成功', `欢迎回来,${values.username}!`);
      form.reset(); // 登录成功后重置表单
    } catch (error) {
      Alert.alert('登录失败', '用户名或密码错误,请重新输入');
    }
  };

  return (
    <SmartKeyboardAvoidingView style={styles.container}>
      <FormContainer style={styles.content}>
        <KeyboardAwareScrollView 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.errorText}>{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.errorText}>{form.errors.password}</Text>
            )}
          </View>

          {/* 记住我复选框 */}
          <TouchableOpacity
            style={styles.checkboxWrap}
            onPress={() => form.setValue('rememberMe', !form.values.rememberMe)}
          >
            <View style={[styles.checkbox, form.values.rememberMe && styles.checkboxChecked]} />
            <Text style={styles.checkboxLabel}>记住我(7天内免登录)</Text>
          </TouchableOpacity>

          {/* 登录按钮 */}
          <TouchableOpacity
            style={[styles.loginBtn, (!form.isValid || form.isSubmitting) && styles.loginBtnDisabled]}
            onPress={form.handleSubmit(onLogin)}
            disabled={!form.isValid || form.isSubmitting}
          >
            {form.isSubmitting ? (
              <ActivityIndicator color="#ffffff" size="small" />
            ) : (
              <Text style={styles.loginBtnText}>登 录</Text>
            )}
          </TouchableOpacity>

          {/* 表单状态展示(调试用) */}
          <View style={styles.formState}>
            <Text style={styles.stateText}>提交次数:{form.submitCount}</Text>
            <Text style={styles.stateText}>表单状态:{form.isDirty ? '已修改' : '未修改'}</Text>
            <Text style={styles.stateText}>验证状态:{form.isValid ? '通过' : '失败'}</Text>
          </View>
        </KeyboardAwareScrollView>
      </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' },
  errorText: { color: '#ff3b30', fontSize: 12, marginTop: 6 },
  checkboxWrap: { flexDirection: 'row', alignItems: 'center', marginBottom: 24 },
  checkbox: {
    width: 20, height: 20,
    borderWidth: 2, borderColor: '#ddd',
    borderRadius: 4, marginRight: 8
  },
  checkboxChecked: { backgroundColor: '#34C759', borderColor: '#34C759' },
  checkboxLabel: { fontSize: 14, color: '#666' },
  loginBtn: {
    backgroundColor: '#34C759',
    borderRadius: 12,
    paddingVertical: 16,
    alignItems: 'center'
  },
  loginBtnDisabled: { backgroundColor: '#cccccc' },
  loginBtnText: { color: '#fff', fontSize: 16, fontWeight: '700' },
  formState: {
    marginTop: 20,
    padding: 16,
    backgroundColor: '#f8f8f8',
    borderRadius: 8
  },
  stateText: { fontSize: 12, color: '#666', marginBottom: 4 }
});

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

tsx 复制代码
// examples/RegistrationForm.tsx
import React from 'react';
import {
  View, Text, TextInput, TouchableOpacity, StyleSheet,
  ScrollView
} from 'react-native';
import { useForm } from '../hooks/useForm';
import { Validator, Validators, CrossFieldValidator } from '../utils/validators';
import { SmartKeyboardAvoidingView } from '../utils/KeyboardManager';

// 注册表单数据类型
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: Validator.combine(
        Validators.required('请再次输入密码'),
        CrossFieldValidator.match('password', '两次输入的密码不一致')
      ),
      // 条件验证:必须同意服务条款
      agreeTerms: Validator.custom((value) => value || '请同意服务条款和隐私政策')
    },
    mode: 'onChangeBlur'
  });

  // 注册提交逻辑
  const onRegister = async (values: RegistrationFormData) => {
    // 模拟注册接口请求
    console.log('注册请求参数:', values);
    // 实际开发中替换为真实API调用
    alert(`注册成功!已向${values.email}发送验证邮件`);
    form.reset();
  };

  return (
    <SmartKeyboardAvoidingView style={styles.container}>
      <ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
        <Text style={styles.title}>创建新账户</Text>

        {/* 邮箱输入框 */}
        <View style={styles.field}>
          <Text style={styles.label}>注册邮箱</Text>
          <TextInput
            style={[styles.input, form.errors.email && styles.inputError]}
            value={form.values.email}
            onChangeText={form.handleChange('email')}
            onBlur={form.handleBlur('email')}
            placeholder="your@email.com"
            keyboardType="email-address"
            autoCapitalize="none"
            autoCorrect={false}
          />
          {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, form.errors.password && styles.inputError]}
            value={form.values.password}
            onChangeText={form.handleChange('password')}
            onBlur={form.handleBlur('password')}
            placeholder="至少8位,包含大小写字母和数字"
            secureTextEntry
            autoCapitalize="none"
          />
          {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, form.errors.confirmPassword && styles.inputError]}
            value={form.values.confirmPassword}
            onChangeText={form.handleChange('confirmPassword')}
            onBlur={form.handleBlur('confirmPassword')}
            placeholder="再次输入密码"
            secureTextEntry
            autoCapitalize="none"
          />
          {form.touched.confirmPassword && form.errors.confirmPassword && (
            <Text style={styles.error}>{form.errors.confirmPassword}</Text>
          )}
        </View>

        {/* 同意服务条款 */}
        <TouchableOpacity
          style={styles.agreeWrap}
          onPress={() => form.setValue('agreeTerms', !form.values.agreeTerms)}
        >
          <View style={[styles.checkbox, form.values.agreeTerms && styles.checkboxChecked]} />
          <Text style={styles.agreeText}>我已阅读并同意《服务条款》和《隐私政策》</Text>
        </TouchableOpacity>
        {form.touched.agreeTerms && form.errors.agreeTerms && (
          <Text style={[styles.error, styles.agreeError]}>{form.errors.agreeTerms}</Text>
        )}

        {/* 注册按钮 */}
        <TouchableOpacity
          style={[
            styles.registerBtn,
            (!form.isValid || form.isSubmitting) && styles.registerBtnDisabled
          ]}
          onPress={form.handleSubmit(onRegister)}
          disabled={!form.isValid || form.isSubmitting}
        >
          <Text style={styles.registerBtnText}>立即注册</Text>
        </TouchableOpacity>
      </ScrollView>
    </SmartKeyboardAvoidingView>
  );
};

// 样式定义
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#ffffff' },
  scroll: { flex: 1, padding: 20 },
  title: { fontSize: 28, fontWeight: 'bold', color: '#333', marginBottom: 24 },
  field: { marginBottom: 20 },
  label: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 8 },
  input: {
    borderWidth: 1,
    borderColor: '#dddddd',
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingVertical: 12,
    fontSize: 16
  },
  inputError: { borderColor: '#ff3b30' },
  error: { color: '#ff3b30', fontSize: 12, marginTop: 4 },
  agreeWrap: { flexDirection: 'row', alignItems: 'flex-start', marginVertical: 8 },
  checkbox: {
    width: 16, height: 16,
    borderWidth: 2, borderColor: '#ddd',
    borderRadius: 2,
    marginRight: 8,
    marginTop: 2
  },
  checkboxChecked: { backgroundColor: '#34C759', borderColor: '#34C759' },
  agreeText: { fontSize: 12, color: '#666', flex: 1 },
  agreeError: { marginLeft: 24, marginTop: 0 },
  registerBtn: {
    backgroundColor: '#34C759',
    paddingVertical: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginTop: 16
  },
  registerBtnDisabled: { backgroundColor: '#cccccc' },
  registerBtnText: { color: '#ffffff', fontSize: 16, fontWeight: '700' }
});

八、最佳实践与性能优化

8.1 useForm表单管理最佳实践

  1. 类型化优先:基于TypeScript为每个表单定义专属的数据接口,充分利用类型校验减少运行时错误;
  2. 验证规则复用:基于Validator工具类封装项目通用的验证规则(如手机号、验证码、密码等),避免重复编写;
  3. 合理选择验证模式 :OpenHarmony平台推荐使用onBlur模式,减少频繁的状态更新和渲染;非OpenHarmony平台可根据需求选择onChange/onChangeBlur
  4. 分离业务与表单逻辑:将表单的状态管理、验证与业务提交逻辑分离,提交逻辑通过回调传入,提高代码复用性;
  5. 利用平台适配工具 :直接使用FormContainerSmartKeyboardAvoidingView等适配组件,无需在业务代码中编写Platform.OS判断;
  6. 表单状态合理利用 :通过isDirty判断表单是否修改,避免无意义的提交;通过isSubmitting防止重复提交。

8.2 OpenHarmony平台适配检查清单

为了保证表单在OpenHarmony平台的流畅运行,开发时需完成以下适配检查:

适配项目 核心检查点 实现方式
焦点管理 输入框外部点击是否能失焦 使用FocusManager+FormContainer组件
键盘处理 键盘弹出时是否遮挡输入框、布局是否错乱 使用SmartKeyboardAvoidingView

✨ 坚持用 清晰的图解 +易懂的硬件架构 + 硬件解析, 让每个知识点都 简单明了 !

🚀 个人主页一只大侠的侠 · CSDN

💬 座右铭 : "所谓成功就是以自己的方式度过一生。"

相关推荐
一只大侠的侠6 小时前
React Native开源鸿蒙跨平台训练营 Day20自定义 useValidator 实现高性能表单验证
flutter·开源·harmonyos
renke33646 小时前
Flutter for OpenHarmony:节奏方块 - 基于时间同步与连击机制的实时音乐游戏系统设计
flutter·游戏
晚霞的不甘7 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频
千逐687 小时前
《Flutter for OpenHarmony:星轨天气的粒子化气象宇宙可视化系统》
flutter
听麟7 小时前
HarmonyOS 6.0+ 跨端智慧政务服务平台开发实战:多端协同办理与电子证照管理落地
笔记·华为·wpf·音视频·harmonyos·政务
前端世界7 小时前
从单设备到多设备协同:鸿蒙分布式计算框架原理与实战解析
华为·harmonyos
晚霞的不甘8 小时前
Flutter for OpenHarmony 实现计算几何:Graham Scan 凸包算法的可视化演示
人工智能·算法·flutter·架构·开源·音视频
猫头虎8 小时前
OpenClaw-VSCode:在 VS Code 里玩转 OpenClaw,远程管理+SSH 双剑合璧
ide·vscode·开源·ssh·github·aigc·ai编程
千逐688 小时前
气象流体场:基于 Flutter for OpenHarmony 的实时天气流体动力学可视化系统
flutter